Skip to main content
Learn how to write and deploy smart contracts on TeQoin L2. TeQoin is 100% EVM-compatible, so your existing Ethereum contracts work without modifications.
Key Points:
  • ✅ Full EVM compatibility (Shanghai version)
  • ✅ Use Solidity 0.4.x - 0.8.x
  • ✅ Same tooling as Ethereum (Hardhat, Foundry, Remix)
  • ✅ No code changes needed for existing contracts

🎯 EVM Compatibility

TeQoin L2 is 100% EVM-compatible, which means:

Same Languages

Solidity, Vyper, YulAll Ethereum contract languages work

Same Tools

Hardhat, Foundry, RemixUse your existing development stack

Same Libraries

OpenZeppelin, ChainlinkAll Ethereum libraries are compatible

Same Bytecode

No Compilation ChangesDeploy existing contracts as-is

Supported Solidity Versions

Version RangeStatusNotes
0.8.x✅ Fully SupportedRecommended for new contracts
0.7.x✅ Fully SupportedWorks perfectly
0.6.x✅ Fully SupportedWorks perfectly
0.5.x✅ Fully SupportedWorks perfectly
0.4.x✅ SupportedOlder version, still works
Recommended: Use Solidity 0.8.20 or later for new contracts. It includes the latest security features and optimizations.

📝 Writing Your First Contract

Let’s write a simple smart contract for TeQoin L2.

Example: Simple Storage Contract

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

/**
 * @title SimpleStorage
 * @dev Store and retrieve a value
 */
contract SimpleStorage {
    uint256 private storedValue;
    
    event ValueChanged(uint256 newValue);
    
    /**
     * @dev Store a value
     * @param value The value to store
     */
    function store(uint256 value) public {
        storedValue = value;
        emit ValueChanged(value);
    }
    
    /**
     * @dev Retrieve the stored value
     * @return The stored value
     */
    function retrieve() public view returns (uint256) {
        return storedValue;
    }
}

🏗️ Contract Patterns for TeQoin

Gas Optimization Tips

TeQoin L2 already has low fees, but you can optimize further:
Pack Variables:
    // ❌ BAD: Uses 3 storage slots
    contract Unoptimized {
        uint8 a;      // Slot 0
        uint256 b;    // Slot 1
        uint8 c;      // Slot 2
    }
    
    // ✅ GOOD: Uses 2 storage slots
    contract Optimized {
        uint8 a;      // Slot 0
        uint8 c;      // Slot 0 (packed)
        uint256 b;    // Slot 1
    }
Use mappings over arrays when appropriate:
    // For frequent random access
    mapping(address => uint256) public balances; // ✅
    
    // For iteration
    address[] public users; // ✅ When you need to loop

🔐 Security Best Practices

Common Vulnerabilities to Avoid

Problem: External calls before state updates can be exploited.
    // ❌ VULNERABLE
    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount);
        
        (bool success,) = msg.sender.call{value: amount}("");
        require(success);
        
        balances[msg.sender] -= amount; // State update AFTER external call
    }
    
    // ✅ SAFE: Checks-Effects-Interactions pattern
    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount);
        
        balances[msg.sender] -= amount; // State update BEFORE external call
        
        (bool success,) = msg.sender.call{value: amount}("");
        require(success);
    }
    
    // ✅ SAFEST: Use ReentrancyGuard from OpenZeppelin
    import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
    
    contract Safe is ReentrancyGuard {
        function withdraw(uint256 amount) public nonReentrant {
            // Safe from reentrancy
        }
    }
Solution: Use Solidity 0.8.x which has built-in overflow checks.
    // Solidity 0.8.x automatically reverts on overflow
    pragma solidity ^0.8.0;
    
    contract Safe {
        function add(uint256 a, uint256 b) public pure returns (uint256) {
            return a + b; // ✅ Safe: reverts on overflow
        }
    }
    
    // For 0.7.x and below, use SafeMath
    pragma solidity ^0.7.0;
    import "@openzeppelin/contracts/math/SafeMath.sol";
    
    contract Safe {
        using SafeMath for uint256;
        
        function add(uint256 a, uint256 b) public pure returns (uint256) {
            return a.add(b); // ✅ Safe with SafeMath
        }
    }
Use proper access control mechanisms:
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "@openzeppelin/contracts/access/AccessControl.sol";
    
    // ✅ Simple owner-only functions
    contract MyContract is Ownable {
        function adminFunction() public onlyOwner {
            // Only owner can call
        }
    }
    
    // ✅ Role-based access control (complex scenarios)
    contract MyContract is AccessControl {
        bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
        
        constructor() {
            _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        }
        
        function mint(address to) public onlyRole(MINTER_ROLE) {
            // Only minters can call
        }
    }
Protect against transaction ordering attacks:
    // Use commit-reveal scheme for sensitive operations
    contract Auction {
        mapping(address => bytes32) public commitments;
        
        // Step 1: Commit
        function commit(bytes32 hash) public {
            commitments[msg.sender] = hash;
        }
        
        // Step 2: Reveal (after commit period)
        function reveal(uint256 value, bytes32 secret) public {
            bytes32 hash = keccak256(abi.encodePacked(value, secret));
            require(hash == commitments[msg.sender], "Invalid reveal");
            // Process bid
        }
    }
Avoid patterns that can be exploited:
    // ❌ VULNERABLE: Relies on external call success
    function refundAll() public {
        for (uint i = 0; i < users.length; i++) {
            users[i].transfer(refunds[users[i]]);
        }
    }
    
    // ✅ SAFE: Pull payment pattern
    mapping(address => uint256) public refunds;
    
    function withdraw() public {
        uint256 amount = refunds[msg.sender];
        refunds[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }

📚 Using Libraries

OpenZeppelin Contracts

The industry-standard library works perfectly on TeQoin:
# Install OpenZeppelin
npm install @openzeppelin/contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }
}

Chainlink

Oracles & VRFPrice feeds and randomness✅ Compatible

Uniswap V2/V3

DEX ContractsAutomated market makers✅ Compatible

AAVE

Lending ProtocolsDeFi primitives✅ Compatible

The Graph

IndexingQuery blockchain data✅ Compatible

🧪 Testing Contracts

Unit Testing with Hardhat

test/SimpleStorage.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("SimpleStorage", function () {
  let simpleStorage;
  let owner;
  
  beforeEach(async function () {
    [owner] = await ethers.getSigners();
    
    const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
    simpleStorage = await SimpleStorage.deploy();
    await simpleStorage.waitForDeployment();
  });
  
  it("Should store and retrieve a value", async function () {
    // Store a value
    await simpleStorage.store(42);
    
    // Retrieve the value
    expect(await simpleStorage.retrieve()).to.equal(42);
  });
  
  it("Should emit ValueChanged event", async function () {
    await expect(simpleStorage.store(100))
      .to.emit(simpleStorage, "ValueChanged")
      .withArgs(100);
  });
});

Testing on TeQoin Testnet

scripts/test-on-testnet.js
const { ethers } = require("hardhat");

async function main() {
  // Connect to deployed contract on testnet
  const contractAddress = "0x...";
  const SimpleStorage = await ethers.getContractAt(
    "SimpleStorage",
    contractAddress
  );
  
  // Test storing a value
  console.log("Storing value 123...");
  const tx = await SimpleStorage.store(123);
  await tx.wait();
  console.log("Value stored!");
  
  // Test retrieving
  const value = await SimpleStorage.retrieve();
  console.log("Retrieved value:", value.toString());
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

🎨 Contract Examples

DeFi: Simple Staking Contract

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SimpleStaking is ReentrancyGuard {
    IERC20 public stakingToken;
    uint256 public rewardRate = 100; // Rewards per second per token
    
    mapping(address => uint256) public stakedBalance;
    mapping(address => uint256) public stakedTime;
    mapping(address => uint256) public rewards;
    
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardClaimed(address indexed user, uint256 reward);
    
    constructor(address _stakingToken) {
        stakingToken = IERC20(_stakingToken);
    }
    
    function stake(uint256 amount) external nonReentrant {
        require(amount > 0, "Cannot stake 0");
        
        // Update rewards before staking
        updateReward(msg.sender);
        
        stakingToken.transferFrom(msg.sender, address(this), amount);
        stakedBalance[msg.sender] += amount;
        stakedTime[msg.sender] = block.timestamp;
        
        emit Staked(msg.sender, amount);
    }
    
    function withdraw(uint256 amount) external nonReentrant {
        require(stakedBalance[msg.sender] >= amount, "Insufficient balance");
        
        // Update rewards before withdrawing
        updateReward(msg.sender);
        
        stakedBalance[msg.sender] -= amount;
        stakingToken.transfer(msg.sender, amount);
        
        emit Withdrawn(msg.sender, amount);
    }
    
    function claimReward() external nonReentrant {
        updateReward(msg.sender);
        
        uint256 reward = rewards[msg.sender];
        require(reward > 0, "No rewards");
        
        rewards[msg.sender] = 0;
        stakingToken.transfer(msg.sender, reward);
        
        emit RewardClaimed(msg.sender, reward);
    }
    
    function updateReward(address user) internal {
        if (stakedBalance[user] > 0) {
            uint256 timeStaked = block.timestamp - stakedTime[user];
            uint256 reward = (stakedBalance[user] * timeStaked * rewardRate) / 1e18;
            rewards[user] += reward;
            stakedTime[user] = block.timestamp;
        }
    }
    
    function pendingReward(address user) public view returns (uint256) {
        if (stakedBalance[user] == 0) return rewards[user];
        
        uint256 timeStaked = block.timestamp - stakedTime[user];
        uint256 newReward = (stakedBalance[user] * timeStaked * rewardRate) / 1e18;
        return rewards[user] + newReward;
    }
}

NFT: Dynamic NFT

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract DynamicNFT is ERC721, Ownable {
    uint256 private _tokenIds;
    
    struct NFTData {
        uint256 level;
        uint256 experience;
        uint256 lastUpdate;
    }
    
    mapping(uint256 => NFTData) public nftData;
    
    constructor() ERC721("DynamicNFT", "DNFT") Ownable(msg.sender) {}
    
    function mint(address to) public onlyOwner returns (uint256) {
        uint256 tokenId = _tokenIds++;
        _safeMint(to, tokenId);
        
        nftData[tokenId] = NFTData({
            level: 1,
            experience: 0,
            lastUpdate: block.timestamp
        });
        
        return tokenId;
    }
    
    function gainExperience(uint256 tokenId, uint256 amount) public {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        
        nftData[tokenId].experience += amount;
        nftData[tokenId].lastUpdate = block.timestamp;
        
        // Level up every 1000 XP
        if (nftData[tokenId].experience >= nftData[tokenId].level * 1000) {
            nftData[tokenId].level++;
        }
    }
    
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        NFTData memory data = nftData[tokenId];
        
        // Generate dynamic metadata based on level
        // In production, you'd generate proper JSON metadata
        return string(abi.encodePacked(
            "data:application/json;base64,",
            _encodeMetadata(tokenId, data)
        ));
    }
    
    function _encodeMetadata(uint256 tokenId, NFTData memory data) 
        internal 
        pure 
        returns (string memory) 
    {
        // Simplified - in production, properly encode JSON
        return "...";
    }
}

🔍 Differences from Ethereum L1

While TeQoin is fully EVM-compatible, there are a few minor considerations:
Block Production:
  • TeQoin produces blocks every 5 seconds (vs 12 seconds on Ethereum)
  • This means block.number increments faster
Impact:
  • Time-based logic using block.number will execute faster
  • Use block.timestamp for time-based logic instead
// ❌ Less reliable on L2
require(block.number > startBlock + 1000); // Not consistent timing

// ✅ Better approach
require(block.timestamp > startTime + 1 hours); // Consistent timing
Lower Gas Costs:
  • Some operations cost less on TeQoin L2
  • Storage is still expensive (same as L1)
Optimization still matters:
  • Even though gas is cheap, optimize for good practice
  • Future-proof your contracts

📖 Learning Resources

Solidity Documentation

Official Solidity docs

OpenZeppelin Contracts

Secure contract library

Ethereum.org

Ethereum developer resources

CryptoZombies

Interactive Solidity tutorial

🎯 Next Steps

Deploy Your Contract

Deploy to TeQoin L2 using Hardhat or Foundry

Verify Your Contract

Verify source code on block explorer

Integration Guide

Integrate contracts with frontend

Network Information

Network configs and endpoints

Ready to deploy? Continue to Deploy Your First Contract