CyclicTokenEngine

Description:

ERC20 token contract with Factory capabilities. Standard implementation for fungible tokens on Ethereum.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "cyclx.sol": {
      "content": "// SPDX-License-Identifier: MIT\r
pragma solidity ^0.8.24;\r
\r
interface IERC20 {\r
    function totalSupply() external view returns (uint256);\r
    function balanceOf(address account) external view returns (uint256);\r
    function transfer(address to, uint256 value) external returns (bool);\r
    function allowance(address owner, address spender) external view returns (uint256);\r
    function approve(address spender, uint256 value) external returns (bool);\r
    function transferFrom(address from, address to, uint256 value) external returns (bool);\r
    event Transfer(address indexed from, address indexed to, uint256 value);\r
    event Approval(address indexed owner, address indexed spender, uint256 value);\r
}\r
\r
library SafeERC20 {\r
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {\r
        (bool ok, bytes memory data) = address(token).call(\r
            abi.encodeWithSelector(token.transferFrom.selector, from, to, value)\r
        );\r
        require(ok && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FROM_FAILED");\r
    }\r
\r
    function safeTransfer(IERC20 token, address to, uint256 value) internal {\r
        (bool ok, bytes memory data) = address(token).call(\r
            abi.encodeWithSelector(token.transfer.selector, to, value)\r
        );\r
        require(ok && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED");\r
    }\r
}\r
\r
contract VersionToken is IERC20 {\r
    string public name;\r
    string public symbol;\r
    uint8 public constant decimals = 18;\r
\r
    uint256 public override totalSupply;\r
\r
    mapping(address => uint256) public override balanceOf;\r
    mapping(address => mapping(address => uint256)) public override allowance;\r
\r
    bytes32 public immutable DOMAIN_SEPARATOR;\r
    bytes32 public constant PERMIT_TYPEHASH = 0xd505accf1f3f3d6c6b379bdc8a2f8f2a78fef8a6a6b7b0b5b2c9f2d2f9a6e46a;\r
    mapping(address => uint256) public nonces;\r
\r
    address public immutable engine;\r
    uint256 public immutable versionNumber;\r
\r
    constructor(string memory _name, string memory _symbol, uint256 _versionNumber, address _engine) {\r
        name = _name;\r
        symbol = _symbol;\r
        versionNumber = _versionNumber;\r
        engine = _engine;\r
\r
        uint256 chainId;\r
        assembly { chainId := chainid() }\r
        DOMAIN_SEPARATOR = keccak256(\r
            abi.encode(\r
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),\r
                keccak256(bytes(_name)),\r
                keccak256(bytes("1")),\r
                chainId,\r
                address(this)\r
            )\r
        );\r
    }\r
\r
    modifier onlyEngine() {\r
        require(msg.sender == engine, "ONLY_ENGINE");\r
        _;\r
    }\r
\r
    function approve(address spender, uint256 value) external override returns (bool) {\r
        allowance[msg.sender][spender] = value;\r
        emit Approval(msg.sender, spender, value);\r
        return true;\r
    }\r
\r
    function transfer(address to, uint256 value) external override returns (bool) {\r
        _transfer(msg.sender, to, value);\r
        return true;\r
    }\r
\r
    function transferFrom(address from, address to, uint256 value) external override returns (bool) {\r
        uint256 allowed = allowance[from][msg.sender];\r
        if (allowed != type(uint256).max) {\r
            require(allowed >= value, "ALLOWANCE");\r
            allowance[from][msg.sender] = allowed - value;\r
            emit Approval(from, msg.sender, allowance[from][msg.sender]);\r
        }\r
        _transfer(from, to, value);\r
        return true;\r
    }\r
\r
    function _transfer(address from, address to, uint256 value) internal {\r
        require(to != address(0), "TO_ZERO");\r
        uint256 bal = balanceOf[from];\r
        require(bal >= value, "BALANCE");\r
        unchecked { balanceOf[from] = bal - value; }\r
        balanceOf[to] += value;\r
        emit Transfer(from, to, value);\r
    }\r
\r
    function _mint(address to, uint256 value) internal {\r
        require(to != address(0), "MINT_TO_ZERO");\r
        totalSupply += value;\r
        balanceOf[to] += value;\r
        emit Transfer(address(0), to, value);\r
    }\r
\r
    function mintFromEngine(address to, uint256 value) external onlyEngine {\r
        _mint(to, value);\r
    }\r
\r
    function permit(\r
        address owner,\r
        address spender,\r
        uint256 value,\r
        uint256 deadline,\r
        uint8 v,\r
        bytes32 r,\r
        bytes32 s\r
    ) external {\r
        require(block.timestamp <= deadline, "PERMIT_DEADLINE");\r
        bytes32 digest = keccak256(\r
            abi.encodePacked(\r
                "\x19\x01",\r
                DOMAIN_SEPARATOR,\r
                keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))\r
            )\r
        );\r
        address recovered = ecrecover(digest, v, r, s);\r
        require(recovered != address(0) && recovered == owner, "BAD_SIG");\r
        allowance[owner][spender] = value;\r
        emit Approval(owner, spender, value);\r
    }\r
}\r
\r
contract CyclicTokenEngine {\r
    using SafeERC20 for IERC20;\r
\r
    uint256 public constant REST_LEN = 30 days;\r
    uint256 public constant STAKE_LEN = 2 days;\r
    uint256 public constant CLAIM_LEN = 2 days;\r
    uint256 public constant CYCLE_LEN = REST_LEN + STAKE_LEN + CLAIM_LEN;\r
\r
    enum Phase { REST, STAKE, CLAIM }\r
\r
    VersionToken public currentToken;\r
    VersionToken public nextToken;\r
    uint256 public currentVersion;\r
    uint64 public cycleIndex;\r
    uint64 public cycleStart;\r
\r
    uint256 private _entered;\r
\r
    struct StakeInfo {\r
        uint256 amount;\r
        bool claimed;\r
        uint64 cycle;\r
    }\r
    mapping(address => StakeInfo) public stakes;\r
\r
    event Staked(address indexed user, uint256 amount, uint64 indexed cycle);\r
    event Claimed(address indexed user, uint256 amount, uint64 indexed cycle, address indexed nextVersion);\r
    event NextVersionDeployed(address indexed token, uint256 indexed versionNumber, string name, string symbol);\r
    event RolledToNextCycle(uint64 newCycleIndex, address indexed newCurrentToken);\r
    event Synced(uint64 indexed cycleIndex, address indexed currentToken, uint8 phaseAfter);\r
\r
    modifier nonReentrant() {\r
        require(_entered == 0, "REENTRANCY");\r
        _entered = 1;\r
        _;\r
        _entered = 0;\r
    }\r
\r
    constructor(string memory baseName, string memory baseSymbol, uint256 v1InitialSupply, address v1Receiver, uint64 firstCycleStartTimestamp) {\r
        require(v1Receiver != address(0), "RECEIVER_ZERO");\r
        require(firstCycleStartTimestamp > 0, "START_ZERO");\r
\r
        string memory nameV1 = string(abi.encodePacked(baseName, "v1"));\r
        string memory symbolV1 = string(abi.encodePacked(baseSymbol, "v1"));\r
        VersionToken v1 = new VersionToken(nameV1, symbolV1, 1, address(this));\r
        v1.mintFromEngine(v1Receiver, v1InitialSupply);\r
\r
        currentToken = v1;\r
        currentVersion = 1;\r
        cycleIndex = 1;\r
        cycleStart = firstCycleStartTimestamp;\r
\r
        emit NextVersionDeployed(address(v1), 1, nameV1, symbolV1);\r
    }\r
\r
    function nowTs() public view returns (uint64) { return uint64(block.timestamp); }\r
\r
    function phaseAt(uint64 ts) public view returns (Phase) {\r
        uint256 d = ts - cycleStart;\r
        if (d < REST_LEN) return Phase.REST;\r
        if (d < REST_LEN + STAKE_LEN) return Phase.STAKE;\r
        if (d < CYCLE_LEN) return Phase.CLAIM;\r
        return Phase.REST;\r
    }\r
\r
    function currentPhase() public view returns (Phase) { return phaseAt(nowTs()); }\r
\r
    function stake(uint256 amount) external nonReentrant {\r
        _stakeInternal(msg.sender, amount);\r
    }\r
\r
    function stakeWithPermit(\r
        uint256 amount,\r
        uint256 deadline,\r
        uint8 v,\r
        bytes32 r,\r
        bytes32 s\r
    ) external nonReentrant {\r
        _rollIfNeeded();\r
        require(currentPhase() == Phase.STAKE, "NOT_STAKE_WINDOW");\r
        require(amount > 0, "ZERO_AMOUNT");\r
        currentToken.permit(msg.sender, address(this), amount, deadline, v, r, s);\r
        _afterPermitStake(msg.sender, amount);\r
    }\r
\r
    function _stakeInternal(address user, uint256 amount) internal {\r
        _rollIfNeeded();\r
        require(currentPhase() == Phase.STAKE, "NOT_STAKE_WINDOW");\r
        require(amount > 0, "ZERO_AMOUNT");\r
\r
        StakeInfo storage s = stakes[user];\r
        if (s.cycle != cycleIndex) {\r
            s.amount = 0; s.claimed = false; s.cycle = cycleIndex;\r
        }\r
\r
        IERC20(address(currentToken)).safeTransferFrom(user, address(this), amount);\r
        s.amount += amount;\r
        emit Staked(user, amount, cycleIndex);\r
    }\r
\r
    function _afterPermitStake(address user, uint256 amount) internal {\r
        StakeInfo storage s = stakes[user];\r
        if (s.cycle != cycleIndex) {\r
            s.amount = 0; s.claimed = false; s.cycle = cycleIndex;\r
        }\r
        IERC20(address(currentToken)).safeTransferFrom(user, address(this), amount);\r
        s.amount += amount;\r
        emit Staked(user, amount, cycleIndex);\r
    }\r
\r
    function claim() external nonReentrant {\r
        _rollIfNeeded();\r
        require(currentPhase() == Phase.CLAIM, "NOT_CLAIM_WINDOW");\r
\r
        StakeInfo storage s = stakes[msg.sender];\r
        require(s.cycle == cycleIndex && !s.claimed, "NO_STAKE_OR_ALREADY");\r
        uint256 amt = s.amount;\r
        require(amt > 0, "ZERO_STAKE");\r
\r
        if (address(nextToken) == address(0)) {\r
            _deployNextVersion();\r
        }\r
\r
        IERC20(address(currentToken)).safeTransfer(msg.sender, amt);\r
        nextToken.mintFromEngine(msg.sender, amt);\r
        s.claimed = true;\r
        s.amount = 0;\r
\r
        emit Claimed(msg.sender, amt, cycleIndex, address(nextToken));\r
    }\r
\r
    function sync() external nonReentrant {\r
        _rollIfNeeded();\r
        emit Synced(cycleIndex, address(currentToken), uint8(currentPhase()));\r
    }\r
\r
    function syncAndInfo() external nonReentrant returns (\r
        Phase phase,\r
        uint64 cycle,\r
        uint64 start,\r
        uint64 restEnd,\r
        uint64 stakeEnd,\r
        uint64 claimEnd\r
    ) {\r
        _rollIfNeeded();\r
        uint64 ts = nowTs();\r
        phase = phaseAt(ts);\r
        cycle = cycleIndex;\r
        start = cycleStart;\r
        restEnd  = start + uint64(REST_LEN);\r
        stakeEnd = restEnd + uint64(STAKE_LEN);\r
        claimEnd = stakeEnd + uint64(CLAIM_LEN);\r
        emit Synced(cycleIndex, address(currentToken), uint8(phase));\r
    }\r
\r
    function phaseInfo() external view returns (Phase phase, uint64 cycle, uint64 start, uint64 restEnd, uint64 stakeEnd, uint64 claimEnd) {\r
        uint64 ts = nowTs();\r
        phase = phaseAt(ts);\r
        cycle = cycleIndex;\r
        start = cycleStart;\r
        restEnd = start + uint64(REST_LEN);\r
        stakeEnd = restEnd + uint64(STAKE_LEN);\r
        claimEnd = stakeEnd + uint64(CLAIM_LEN);\r
    }\r
\r
    function _deployNextVersion() internal {\r
        uint256 vNext = currentVersion + 1;\r
        string memory baseName = _stripSuffix(currentToken.name());\r
        string memory baseSymbol = _stripSuffix(currentToken.symbol());\r
        string memory nameNext = string(abi.encodePacked(baseName, _toV(vNext)));\r
        string memory symbolNext = string(abi.encodePacked(baseSymbol, _toV(vNext)));\r
        VersionToken v = new VersionToken(nameNext, symbolNext, vNext, address(this));\r
        nextToken = v;\r
        emit NextVersionDeployed(address(v), vNext, nameNext, symbolNext);\r
    }\r
\r
    function _rollIfNeeded() internal {\r
        uint64 ts = nowTs();\r
        while (ts >= cycleStart + uint64(CYCLE_LEN)) {\r
            if (address(nextToken) != address(0)) {\r
                currentToken = nextToken;\r
                currentVersion += 1;\r
                nextToken = VersionToken(address(0));\r
            } else {\r
                _deployNextVersion();\r
                currentToken = nextToken;\r
                currentVersion += 1;\r
                nextToken = VersionToken(address(0));\r
            }\r
            cycleIndex += 1;\r
            cycleStart += uint64(CYCLE_LEN);\r
            emit RolledToNextCycle(cycleIndex, address(currentToken));\r
        }\r
    }\r
\r
    function _stripSuffix(string memory s) internal pure returns (string memory) {\r
        bytes memory b = bytes(s);\r
        if (b.length == 0) return s;\r
        uint256 i = b.length;\r
        while (i > 0 && _isDigit(b[i-1])) { i--; }\r
        if (i > 0 && b[i-1] == bytes1("v")) {\r
            bytes memory out = new bytes(i-1);\r
            for (uint256 j = 0; j < i-1; j++) out[j] = b[j];\r
            return string(out);\r
        }\r
        return s;\r
    }\r
\r
    function _isDigit(bytes1 c) private pure returns (bool) {\r
        return (c >= 0x30 && c <= 0x39);\r
    }\r
\r
    function _toV(uint256 n) private pure returns (string memory) {\r
        return string(abi.encodePacked("v", _toString(n)));\r
    }\r
\r
    function _toString(uint256 v) private pure returns (string memory) {\r
        if (v == 0) return "0";\r
        uint256 tmp = v; uint256 len;\r
        while (tmp != 0) { len++; tmp /= 10; }\r
        bytes memory b = new bytes(len);\r
        while (v != 0) { b[--len] = bytes1(uint8(48 + v % 10)); v /= 10; }\r
        return string(b);\r
    }\r
}"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": false,
      "runs": 200
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
ERC20, Token, Factory|addr:0xfc5d13e3475de19f375584f79ff003c573dc2048|verified:true|block:23698683|tx:0x21b145c29455d65bf14e62b71f5f3e869a0c1200f9f6052df844522b0f803b71|first_check:1761933603

Submitted on: 2025-10-31 19:00:04

Comments

Log in to comment.

No comments yet.