Skip to content

Smart Contracts Guide

Learn how to create, deploy, and interact with smart contracts in Civra.

Introduction

Smart contracts are self-executing programs on the blockchain. Civra makes it easy to create, test, and deploy secure smart contracts.

Creating Your First Contract

Using AI

Simply describe what you want:

Create an ERC-20 token called MyToken with 1 million supply

Civra will generate:

  • Complete Solidity contract
  • Deployment script
  • Basic tests
  • Frontend integration

Starting from Templates

Browse pre-built templates:

  • ERC-20 Token
  • ERC-721 NFT
  • ERC-1155 Multi-Token
  • Staking Contract
  • DAO Governance
  • Marketplace
  • MultiSig Wallet

Contract Standards

ERC-20 (Fungible Tokens)

Standard for cryptocurrencies and utility tokens.

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

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

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

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

ERC-721 (NFTs)

Standard for non-fungible tokens.

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

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

contract MyNFT is ERC721, ERC721URIStorage, Ownable {
    uint256 private _tokenIdCounter;

    constructor() ERC721("MyNFT", "MNFT") Ownable(msg.sender) {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _tokenIdCounter++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

ERC-1155 (Multi-Token)

Standard for managing multiple token types.

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

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

contract GameItems is ERC1155, Ownable {
    uint256 public constant GOLD = 0;
    uint256 public constant SILVER = 1;
    uint256 public constant SWORD = 2;

    constructor() ERC1155("https://game.example/api/item/{id}.json")
        Ownable(msg.sender) {
        _mint(msg.sender, GOLD, 10**18, "");
        _mint(msg.sender, SILVER, 10**27, "");
        _mint(msg.sender, SWORD, 1, "");
    }

    function mint(address account, uint256 id, uint256 amount)
        public
        onlyOwner
    {
        _mint(account, id, amount, "");
    }
}

Advanced Patterns

Staking Contract

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

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

contract StakingPool is ReentrancyGuard {
    IERC20 public stakingToken;
    IERC20 public rewardToken;

    uint256 public rewardRate = 100; // tokens per second
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;
    mapping(address => uint256) public balances;

    uint256 private _totalSupply;

    constructor(address _stakingToken, address _rewardToken) {
        stakingToken = IERC20(_stakingToken);
        rewardToken = IERC20(_rewardToken);
    }

    function stake(uint256 amount) external nonReentrant {
        require(amount > 0, "Cannot stake 0");
        _totalSupply += amount;
        balances[msg.sender] += amount;
        stakingToken.transferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(amount > 0, "Cannot withdraw 0");
        _totalSupply -= amount;
        balances[msg.sender] -= amount;
        stakingToken.transfer(msg.sender, amount);
    }

    function claimReward() external nonReentrant {
        uint256 reward = rewards[msg.sender];
        if (reward > 0) {
            rewards[msg.sender] = 0;
            rewardToken.transfer(msg.sender, reward);
        }
    }
}

Marketplace Contract

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

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract NFTMarketplace is ReentrancyGuard {
    struct Listing {
        address seller;
        uint256 price;
        bool active;
    }

    mapping(address => mapping(uint256 => Listing)) public listings;
    uint256 public feePercent = 2; // 2% fee

    event Listed(
        address indexed nftContract,
        uint256 indexed tokenId,
        address seller,
        uint256 price
    );

    event Sold(
        address indexed nftContract,
        uint256 indexed tokenId,
        address seller,
        address buyer,
        uint256 price
    );

    function list(address nftContract, uint256 tokenId, uint256 price)
        external
    {
        require(price > 0, "Price must be > 0");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);

        listings[nftContract][tokenId] = Listing({
            seller: msg.sender,
            price: price,
            active: true
        });

        emit Listed(nftContract, tokenId, msg.sender, price);
    }

    function buy(address nftContract, uint256 tokenId)
        external
        payable
        nonReentrant
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(listing.active, "Not listed");
        require(msg.value >= listing.price, "Insufficient payment");

        uint256 fee = (listing.price * feePercent) / 100;
        uint256 sellerProceeds = listing.price - fee;

        listings[nftContract][tokenId].active = false;

        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        payable(listing.seller).transfer(sellerProceeds);

        emit Sold(nftContract, tokenId, listing.seller, msg.sender, listing.price);
    }
}

Security Best Practices

Access Control

solidity
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract SecureContract is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ADMIN_ROLE, msg.sender);
    }

    function adminFunction() public onlyRole(ADMIN_ROLE) {
        // Admin only
    }

    function mint() public onlyRole(MINTER_ROLE) {
        // Minters only
    }
}

Reentrancy Protection

solidity
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureWithdraw is ReentrancyGuard {
    function withdraw() external nonReentrant {
        // Safe from reentrancy
    }
}

Input Validation

solidity
function transfer(address to, uint256 amount) public {
    require(to != address(0), "Invalid address");
    require(amount > 0, "Amount must be > 0");
    require(balances[msg.sender] >= amount, "Insufficient balance");
    // ... transfer logic
}

Testing Contracts

Civra automatically generates tests:

javascript
describe("MyToken", function() {
  it("Should mint initial supply", async function() {
    const [owner] = await ethers.getSigners()
    const MyToken = await ethers.getContractFactory("MyToken")
    const token = await MyToken.deploy()

    const balance = await token.balanceOf(owner.address)
    expect(balance).to.equal(ethers.parseEther("1000000"))
  })
})

Deployment

Deploy to Testnet

  1. Select network (Sepolia, Mumbai, etc.)
  2. Review gas estimates
  3. Confirm deployment
  4. Get contract address

Verify on Etherscan

Civra automatically verifies your contracts:

bash
npx hardhat verify --network sepolia DEPLOYED_CONTRACT_ADDRESS

Gas Optimization

Tips

  1. Use uint256 instead of smaller uints
  2. Pack variables in storage
  3. Use calldata for function parameters
  4. Batch operations
  5. Use events instead of storage when possible

Example

solidity
// ❌ Inefficient
function badMint(address[] memory recipients) public {
    for (uint i = 0; i < recipients.length; i++) {
        _mint(recipients[i], 1);
    }
}

// ✅ Optimized
function goodMint(address[] calldata recipients) public {
    uint256 length = recipients.length;
    for (uint256 i; i < length;) {
        _mint(recipients[i], 1);
        unchecked { ++i; }
    }
}

Common Issues

Transaction Failing?

  • Check gas limits
  • Verify approvals
  • Confirm correct network
  • Review contract state

High Gas Costs?

  • Optimize loops
  • Batch transactions
  • Use events
  • Pack storage

Next Steps

Built with Civra - Web3 Development Platform