Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions contracts/FactoryTokenV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";

/// @title FactoryTokenV2
/// @notice Extended ERC-20 deployed by TokenFactoryV2.
/// Adds EIP-2612 Permit, optional supply cap, and freeze controls.
/// @dev Combines OpenZeppelin extensions for gasless approvals,
/// capped supply, and admin-pausable transfers.
contract FactoryTokenV2 is ERC20, ERC20Permit, ERC20Capped, Ownable, Pausable {
error FactoryTokenV2__CapExceeded(uint256 cap, uint256 requested);

uint8 private immutable _decimals;
bool private immutable _pauseEnabled;

/// @param _name Token name
/// @param _symbol Token symbol
/// @param _decimals_ Token decimals (e.g. 18)
/// @param _initialSupply Initial amount minted to deployer
/// @param _cap Maximum total supply (0 = unlimited)
/// @param _owner Admin address (pause, unpause, ownership)
/// @param _permitEnabled Whether EIP-2612 permit is active
/// @param _pauseEnabled Whether admin-pause controls are active
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals_,
uint256 _initialSupply,
uint256 _cap,
address _owner,
bool _permitEnabled,
bool _pauseEnabled
)
ERC20(_name, _symbol)
ERC20Permit(_permitEnabled ? _name : "") // permit salt must be non-empty when enabled
ERC20Capped(_cap == 0 ? type(uint256).max : _cap)
Comment on lines +40 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Check how permitEnabled is used in FactoryTokenV2:"
rg -n "permitEnabled|ERC20Permit\\(|function permit\\(" contracts/FactoryTokenV2.sol

echo
echo "2) Inspect OpenZeppelin ERC20Permit constructor + permit function:"
PERMIT_FILE="$(fd -i 'ERC20Permit.sol' | head -n1 || true)"
if [ -z "${PERMIT_FILE:-}" ]; then
  echo "ERC20Permit.sol not found in-repo (dependency may be external)."
  exit 0
fi
echo "Found: $PERMIT_FILE"
rg -n "constructor\\(|function permit\\(|function DOMAIN_SEPARATOR\\(" "$PERMIT_FILE"

Repository: sentrix-labs/canonical-contracts

Length of output: 461


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1) Read full FactoryTokenV2 constructor and permit-related code ==="
cat -n contracts/FactoryTokenV2.sol | head -100

echo
echo "=== 2) Check for any permit overrides in FactoryTokenV2 ==="
rg -n "override.*permit|function permit" contracts/FactoryTokenV2.sol || echo "No permit overrides found"

echo
echo "=== 3) Search for tests referencing permit behavior ==="
fd -e test.ts -e test.sol -e spec.ts -o -path '*test*' -name '*FactoryToken*' | head -5

Repository: sentrix-labs/canonical-contracts

Length of output: 4966


🌐 Web query:

OpenZeppelin ERC20Permit constructor empty name parameter behavior domain separator permit function

💡 Result:

In OpenZeppelin's ERC20Permit implementation, the constructor accepts a string parameter name which is passed to the EIP712 base contract [1][2]. If you provide an empty string ("") as the name parameter, the contract will function technically, but it will result in a domain separator that is derived from an empty name [1][3]. Specifically, the EIP712 domain separator is calculated using the hash of the name, the version, the chain ID, and the contract address [4][3][5]. If the name is an empty string, its hash (keccak256(bytes(""))) is 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470. This value will be used in the domain separator calculation [3]. While this is not a functional error that prevents the permit function from working, it is generally discouraged for the following reasons: 1. Domain Uniqueness: The name parameter is intended to provide a human-readable identifier for the signing domain [4][6]. Using an empty string makes the domain separator less unique and potentially confusing for users or wallet interfaces that display the signing domain [6][5]. 2. Best Practices: OpenZeppelin documentation explicitly recommends using the same name that is defined as the ERC-20 token name [1][2]. This ensures that the domain separator is tied to the specific identity of the token, which is a core purpose of EIP-712 domain separation [5]. 3. Wallet Compatibility: Wallets and other EIP-712-compliant tools often use the name field to present the signing context to the user [6][5]. An empty name may lead to poor user experience or security warnings in some wallet implementations that expect a descriptive name [6]. In summary, passing an empty string will not cause the contract to revert or fail to generate a domain separator, but it will result in a non-descriptive domain separator that deviates from standard practices and may negatively impact user experience and security clarity [1][6][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for test files referencing FactoryTokenV2 or permit ==="
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.test.js" -o -name "*.test.sol" \) 2>/dev/null | head -20

echo
echo "=== Search for permit-related tests in test directory ==="
if [ -d "test" ] || [ -d "tests" ]; then
  rg -l "permit|FactoryTokenV2" --type ts --type js test* 2>/dev/null | head -10 || echo "No matches"
fi

echo
echo "=== Verify _permitEnabled is stored as immutable or used elsewhere ==="
rg -n "_permitEnabled" contracts/FactoryTokenV2.sol

Repository: sentrix-labs/canonical-contracts

Length of output: 484


permitEnabled flag does not disable permit() in its current implementation.

The constructor passes _permitEnabled ? _name : "" to ERC20Permit, but this does not prevent the inherited permit() function from being called and working normally. An empty domain name still produces a valid (though non-descriptive) domain separator. This breaks the advertised feature toggle: users who deploy with _permitEnabled=false will still have permit functionality available.

Additionally, _permitEnabled is read once during construction and never stored, making it impossible to query whether permit was intended to be enabled.

Suggested fix
 contract FactoryTokenV2 is ERC20, ERC20Permit, ERC20Capped, Ownable, Pausable {
     error FactoryTokenV2__CapExceeded(uint256 cap, uint256 requested);
+    error FactoryTokenV2__PermitDisabled();

     uint8 private immutable _decimals;
     bool  private immutable _pauseEnabled;
+    bool  private immutable _permitEnabledFlag;
@@
     {
         _decimals = _decimals_;
         _pauseEnabled = _pauseEnabled;
+        _permitEnabledFlag = _permitEnabled;
@@
     }
+
+    function permit(
+        address owner,
+        address spender,
+        uint256 value,
+        uint256 deadline,
+        uint8 v,
+        bytes32 r,
+        bytes32 s
+    ) public override {
+        if (!_permitEnabledFlag) revert FactoryTokenV2__PermitDisabled();
+        super.permit(owner, spender, value, deadline, v, r, s);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/FactoryTokenV2.sol` around lines 40 - 41, Store the _permitEnabled
flag in the contract state (e.g., a public bool permitEnabled set in the
constructor) and explicitly disable EIP‑2612 by overriding the inherited
permit(...) function to revert (or require(permitEnabled)) when permitEnabled is
false; keep calling ERC20Permit(...) in the constructor for domain setup if you
want but rely on the stored permitEnabled flag and the overridden permit method
to actually prevent use when disabled, and expose the flag via a public getter
so callers can query whether permit was intended to be enabled.

Ownable(_owner)
{
_decimals = _decimals_;
_pauseEnabled = _pauseEnabled;

// _cap == 0 means unlimited — max uint256 cap is effectively unlimited.
if (_initialSupply > 0) {
_mint(_owner, _initialSupply);
}
}

/// @notice Returns the token's configured decimals.
function decimals() public view override returns (uint8) {
return _decimals;
}

// ─── Pause controls ────────────────────────────────────────────

/// @notice Pause all token transfers. Only owner.
/// @dev Reverts if pause controls were disabled at deploy time.
function pause() external onlyOwner {
require(_pauseEnabled, "FactoryTokenV2: PAUSE_DISABLED");
_pause();
}

/// @notice Unpause token transfers. Only owner.
/// @dev Reverts if pause controls were disabled at deploy time.
function unpause() external onlyOwner {
require(_pauseEnabled, "FactoryTokenV2: PAUSE_DISABLED");
_unpause();
}

// ─── Supply cap override ───────────────────────────────────────

/// @notice Mint new tokens up to the supply cap. Only owner.
/// @dev Reverts if mint would exceed cap.
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}

// ─── Internal overrides ────────────────────────────────────────

/// @dev Hook into ERC20 _update: enforce cap + pause on all transfers.
/// Pausable v5.3.0 uses modifier pattern, not _update override,
/// so we call _requireNotPaused() manually.
function _update(
address from,
address to,
uint256 value
)
internal
override(ERC20, ERC20Capped)
{
_requireNotPaused();
super._update(from, to, value);
}

/// @notice Returns the current supply cap. Max uint256 = unlimited.
/// @dev Exposed for transparency. Use `isCapped()` to check if a cap is set.
function cap() public view override(ERC20Capped) returns (uint256) {
return ERC20Capped.cap();
}

/// @notice Returns true if a supply cap is enforced.
function isCapped() public view returns (bool) {
return ERC20Capped.cap() != type(uint256).max;
}
}
65 changes: 65 additions & 0 deletions contracts/TokenFactoryV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

import {FactoryTokenV2} from "./FactoryTokenV2.sol";
import {ITokenFactoryV2} from "./interfaces/ITokenFactoryV2.sol";

/// @title Sentrix TokenFactoryV2
/// @author Sentrix Labs
/// @notice Deploys extended ERC-20 tokens with Permit, optional supply cap,
/// and freeze controls. Sits next to TokenFactory (v1), not replacing it.
/// @dev Tracks deployed-token-by-deployer for discovery.
contract TokenFactoryV2 is ITokenFactoryV2 {
uint256 public constant MAX_NAME_LENGTH = 64;
uint256 public constant MAX_SYMBOL_LENGTH = 16;

mapping(address => address[]) public deployedTokens;

/// @notice Deploy a new FactoryTokenV2.
/// @param name Token name
/// @param symbol Token symbol
/// @param decimals_ Token decimals (e.g. 18)
/// @param initialSupply Amount minted to `msg.sender` on deploy
/// @param cap Maximum total supply (0 = unlimited)
/// @param permitEnabled Whether EIP-2612 permit is enabled
/// @param pauseEnabled Whether admin-pause controls are enabled
/// @return token Address of the new FactoryTokenV2
function deployToken(
string calldata name,
string calldata symbol,
uint8 decimals_,
uint256 initialSupply,
uint256 cap,
bool permitEnabled,
bool pauseEnabled
) external returns (address token) {
require(bytes(name).length > 0 && bytes(name).length <= MAX_NAME_LENGTH, "TokenFactoryV2: BAD_NAME");
require(bytes(symbol).length > 0 && bytes(symbol).length <= MAX_SYMBOL_LENGTH, "TokenFactoryV2: BAD_SYMBOL");

// If cap is set, initialSupply must not exceed it.
if (cap > 0) {
require(initialSupply <= cap, "TokenFactoryV2: CAP_EXCEEDED");
}

FactoryTokenV2 t = new FactoryTokenV2(
name, symbol, decimals_, initialSupply, cap, msg.sender,
permitEnabled, pauseEnabled
);
token = address(t);
deployedTokens[msg.sender].push(token);
emit TokenDeployed(
token, msg.sender, name, symbol, decimals_,
initialSupply, cap, permitEnabled, pauseEnabled
);
}

/// @notice All tokens deployed by `owner`.
function tokensOf(address owner) external view returns (address[] memory) {
return deployedTokens[owner];
}

/// @notice Number of tokens deployed by `owner`.
function tokenCount(address owner) external view returns (uint256) {
return deployedTokens[owner].length;
}
}
139 changes: 139 additions & 0 deletions contracts/Vault4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

/// @title Vault4626
/// @notice Minimal canonical ERC-4626 vault for the Sentrix ecosystem.
/// Accepts an underlying ERC-20 asset and mints vault shares (1:1 start).
/// @dev OpenZeppelin-derived. No yield strategy — yield sources extend this.
/// Inflation-attack guard: deploys with a small dead-share mint to make
/// donation attacks unprofitable (virtual offset pattern).
contract Vault4626 is ERC4626 {
using Math for uint256;

/// @notice Emitted when the vault receives a donation (pure asset transfer, no shares minted).
event Donation(address indexed donor, uint256 amount);

error Vault4626__ZeroDeposit();

/// @param _asset Underlying ERC-20 token address.
/// @param _name Vault share token name.
/// @param _symbol Vault share token symbol.
constructor(IERC20 _asset, string memory _name, string memory _symbol)
ERC4626(_asset)
ERC20(_name, _symbol)
{
// Inflation-attack guard: mint 1e3 dead shares to make donation attacks
// prohibitively expensive. With a 1e3 share floor, an attacker needs
// 1e3 * share_price of underlying to front-run, making the attack cost
// exceed any reasonable first-deposit value.
_mint(address(0xdead), 1e3);
}

// ─── Overrides for correct rounding ─────────────────────────────

/// @inheritdoc ERC4626
/// @dev Rounds UP (protects vault). Uses mulDiv with Math.Rounding.Ceil.
function _convertToShares(uint256 assets, Math.Rounding rounding)
internal
view
override
returns (uint256)
{
uint256 supply = totalSupply();
// If no live supply beyond our dead shares, shares = assets (1:1 after offset)
if (supply == 1e3) return assets;

return _tryMulDiv(assets, supply - 1e3, totalAssets(), rounding);
}

/// @inheritdoc ERC4626
/// @dev Rounds DOWN (protects vault). Uses mulDiv with Math.Rounding.Floor.
function _convertToAssets(uint256 shares, Math.Rounding rounding)
internal
view
override
returns (uint256)
{
uint256 supply = totalSupply();
// If only dead shares exist, assets = shares (1:1)
if (supply == 1e3) return shares;

return _tryMulDiv(shares, totalAssets(), supply - 1e3, rounding);
}
Comment on lines +42 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: Dead-share accounting allows first depositor to steal donated assets.

The conversion logic returns 1:1 when supply == 1e3 regardless of totalAssets. If assets are donated before the first real deposit, the first depositor can drain them:

  1. After constructor: supply=1000, totalAssets=0
  2. Donation of 10,000 assets: totalAssets=10000, supply=1000
  3. User deposits 1,000: supply==1e3 → user receives 1,000 shares (1:1)
  4. Now: supply=2000, totalAssets=11000
  5. User redeems 1,000 shares: assets = 1000 * 11000 / (2000-1000) = 11000

The user deposited 1,000 but withdraws 11,000—stealing the entire donation.

The fix should use a proper virtual offset that adds to both numerator and denominator rather than subtracting dead shares only from supply. Alternatively, when supply == 1e3, compute shares as assets * 1e3 / totalAssets (if totalAssets > 0) to properly dilute against existing assets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/Vault4626.sol` around lines 42 - 68, The current dead-share
shortcut in _convertToShares and _convertToAssets returns a 1:1 mapping when
totalSupply() == 1e3, letting the first depositor steal donated assets; instead,
when supply == 1e3 and totalAssets() > 0 compute conversions proportionally
using the dead-share offset (i.e., treat the virtual offset as added to both
sides): in _convertToShares replace the `if (supply == 1e3) return assets;`
branch with a proportional calculation like assets * supply / totalAssets (use
_tryMulDiv and Math.Rounding as in the rest), and in _convertToAssets replace
the `if (supply == 1e3) return shares;` branch with shares * totalAssets /
supply (again via _tryMulDiv); keep the special-case zero-totalAssets behavior
(if totalAssets() == 0 fall back to 1:1) and reuse the DEAD_SHARES constant
(1e3), totalSupply(), totalAssets(), _convertToShares, and _convertToAssets
identifiers to locate changes.


/// @inheritdoc ERC4626
function totalAssets() public view override returns (uint256) {
return IERC20(asset()).balanceOf(address(this));
}

/// @inheritdoc ERC4626
function maxDeposit(address) public view override returns (uint256) {
return type(uint256).max;
}

/// @inheritdoc ERC4626
function maxMint(address) public view override returns (uint256) {
return type(uint256).max;
}

// ─── Hook pattern for yield extensions ──────────────────────────

/// @notice Hook called after shares are minted (deposit/mint).
/// @dev Override in yield-bearing children to route assets to strategies.
function _afterDeposit(address caller, uint256 assets, uint256 shares) internal virtual {}

/// @notice Hook called before shares are burned (withdraw/redeem).
/// @dev Override in yield-bearing children to pull assets from strategies.
function _beforeWithdraw(address caller, uint256 assets, uint256 shares) internal virtual {}

// ─── Donation support ──────────────────────────────────────────

/// @notice Donate underlying assets to the vault. No shares minted in return.
/// Donations increase the share price for all existing shareholders.
function donate(uint256 assets) external {
if (assets == 0) revert Vault4626__ZeroDeposit();
SafeERC20.safeTransferFrom(IERC20(asset()), msg.sender, address(this), assets);
emit Donation(msg.sender, assets);
}

// ─── Internal ──────────────────────────────────────────────────

/// @dev Override deposit to call the after-hook.
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets);
_mint(receiver, shares);
_afterDeposit(caller, assets, shares);
emit Deposit(caller, receiver, assets, shares);
}
Comment on lines +108 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Consider adding ReentrancyGuard for defense-in-depth.

While the current implementation has no obvious reentrancy exploit, the _deposit function performs external calls before state changes (safeTransferFrom before _mint). If the underlying asset has transfer callbacks (ERC-777) and derived contracts add stateful logic in _afterDeposit, unexpected interactions could arise.

Adding nonReentrant to deposit/withdraw entry points would provide defense-in-depth as yield-strategy children extend this contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/Vault4626.sol` around lines 108 - 113, Add ReentrancyGuard
inheritance and apply nonReentrant to the external entry points so
deposits/withdrawals cannot be reentered: import OpenZeppelin's ReentrancyGuard,
have the contract inherit ReentrancyGuard, and add the nonReentrant modifier to
the public deposit and withdraw functions (the external entry points that call
internal _deposit and _withdraw/_burn logic); leave internal helpers like
_deposit and _afterDeposit unchanged so derived contracts still run but are
protected by the nonReentrant guard at the boundaries.


/// @dev Override withdraw to call the before-hook.
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
internal
override
{
_beforeWithdraw(caller, assets, shares);
if (caller != owner) {
_spendAllowance(owner, caller, shares);
}
_burn(owner, shares);
SafeERC20.safeTransfer(IERC20(asset()), receiver, assets);
emit Withdraw(caller, receiver, owner, assets, shares);
}

/// @dev Safe mulDiv that returns 0 when operands overflow, with configurable rounding.
function _tryMulDiv(
uint256 x,
uint256 y,
uint256 denominator,
Math.Rounding rounding
) internal pure returns (uint256) {
if (denominator == 0) return 0;
return x.mulDiv(y, denominator, rounding);
}
Comment on lines +129 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Silent zero return on division-by-zero can cause loss of funds.

When denominator == 0, this returns 0 rather than reverting. If a depositor calls when totalAssets() == 0 but supply > 1e3 (possible with fee-on-transfer tokens or edge scenarios), they would receive 0 shares for their deposit—silently losing funds.

Prefer reverting on zero denominator to fail-safe rather than fail-silent:

Proposed fix
 function _tryMulDiv(
     uint256 x,
     uint256 y,
     uint256 denominator,
     Math.Rounding rounding
 ) internal pure returns (uint256) {
-    if (denominator == 0) return 0;
+    require(denominator > 0, "Vault4626: zero denominator");
     return x.mulDiv(y, denominator, rounding);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/Vault4626.sol` around lines 129 - 138, The helper _tryMulDiv
currently returns 0 when denominator == 0 which can silently lose funds; change
it to revert instead: in function _tryMulDiv(uint256 x, uint256 y, uint256
denominator, Math.Rounding rounding) check denominator and revert (use a clear
message or a custom error like DivisionByZero) if denominator == 0, otherwise
return x.mulDiv(y, denominator, rounding); keep the rest of the logic and
signature unchanged so callers of _tryMulDiv detect the failure instead of
receiving a silent zero.

}
45 changes: 45 additions & 0 deletions contracts/interfaces/ITokenFactoryV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

/// @title ITokenFactoryV2
/// @author Sentrix Labs
/// @notice Deploys extended ERC-20 tokens with Permit, optional supply cap,
/// and freeze controls.
interface ITokenFactoryV2 {
event TokenDeployed(
address indexed token,
address indexed owner,
string name,
string symbol,
uint8 decimals,
uint256 initialSupply,
uint256 cap,
bool permitEnabled,
bool pauseEnabled
);

/// @notice Deploy a new FactoryTokenV2 with `initialSupply` minted to caller.
/// @param name Token name (1-64 chars)
/// @param symbol Token symbol (1-16 chars)
/// @param decimals_ Token decimals (e.g. 18)
/// @param initialSupply Amount minted to `msg.sender` on deploy
/// @param cap Maximum total supply (0 = unlimited)
/// @param permitEnabled Whether EIP-2612 permit is enabled
/// @param pauseEnabled Whether admin-pause is enabled
/// @return token Address of the new FactoryTokenV2
function deployToken(
string calldata name,
string calldata symbol,
uint8 decimals_,
uint256 initialSupply,
uint256 cap,
bool permitEnabled,
bool pauseEnabled
) external returns (address token);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// @notice Returns all tokens deployed by `owner`.
function tokensOf(address owner) external view returns (address[] memory);

/// @notice Number of tokens deployed by `owner`.
function tokenCount(address owner) external view returns (uint256);
}
Loading
Loading