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