โ ๏ธ Never commit your .env file to version control
Token Contract Development
Step 1: Generate a Token Contract
axicov generate erc20 --name RewardsToken
Answer the prompts:
Token Name: Rewards Token
Token Symbol: RWRD
Initial Supply: 10000000
Should the token be mintable? Yes
Should the token be burnable? Yes
Step 2: Review the Generated Contract
Examine the generated contract at contracts/RewardsToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract RewardsToken is ERC20, ERC20Burnable, Pausable, Ownable {
constructor() ERC20("Rewards Token", "RWRD") {
_mint(msg.sender, 10000000 * 10 ** decimals());
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal
whenNotPaused
override
{
super._beforeTokenTransfer(from, to, amount);
}
}
Step 3: Customize the Token Contract
Let's add a rewards distribution feature. Edit contracts/RewardsToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract RewardsToken is ERC20, ERC20Burnable, Pausable, Ownable {
// Rewards configuration
uint256 public rewardRate = 100; // 100 tokens per distribution
mapping(address => uint256) public lastRewardTimestamp;
uint256 public rewardInterval = 1 days;
event RewardClaimed(address indexed user, uint256 amount);
constructor() ERC20("Rewards Token", "RWRD") {
_mint(msg.sender, 10000000 * 10 ** decimals());
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
/**
* @dev Claims available rewards for the caller
* @return Amount of rewards claimed
*/
function claimRewards() public whenNotPaused returns (uint256) {
require(
block.timestamp >= lastRewardTimestamp[msg.sender] + rewardInterval,
"RewardsToken: too early to claim rewards"
);
lastRewardTimestamp[msg.sender] = block.timestamp;
uint256 rewardAmount = rewardRate * 10 ** decimals();
_mint(msg.sender, rewardAmount);
emit RewardClaimed(msg.sender, rewardAmount);
return rewardAmount;
}
/**
* @dev Updates the reward rate
* @param newRate New tokens per distribution
*/
function setRewardRate(uint256 newRate) public onlyOwner {
rewardRate = newRate;
}
/**
* @dev Updates the reward interval
* @param newInterval New interval in seconds
*/
function setRewardInterval(uint256 newInterval) public onlyOwner {
rewardInterval = newInterval;
}
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal
whenNotPaused
override
{
super._beforeTokenTransfer(from, to, amount);
}
}
Testing the Contract
Step 1: Generate and Customize Tests
Axicov generated basic tests, but let's enhance them for our reward functionality. Edit test/RewardsToken-test.ts:
import { expect } from "chai";
import { ethers } from "hardhat";
import { time } from "@nomicfoundation/hardhat-network-helpers";
describe("RewardsToken", function () {
it("Should deploy with correct name and symbol", async function () {
const RewardsToken = await ethers.getContractFactory("RewardsToken");
const token = await RewardsToken.deploy();
await token.deployed();
expect(await token.name()).to.equal("Rewards Token");
expect(await token.symbol()).to.equal("RWRD");
});
it("Should have the correct initial supply", async function () {
const RewardsToken = await ethers.getContractFactory("RewardsToken");
const token = await RewardsToken.deploy();
await token.deployed();
const totalSupply = await token.totalSupply();
expect(totalSupply).to.equal(ethers.utils.parseEther("10000000"));
});
it("Should allow minting new tokens", async function () {
const [owner, addr1] = await ethers.getSigners();
const RewardsToken = await ethers.getContractFactory("RewardsToken");
const token = await RewardsToken.deploy();
await token.deployed();
const initialBalance = await token.balanceOf(addr1.address);
const mintAmount = ethers.utils.parseEther("1000");
await token.mint(addr1.address, mintAmount);
const newBalance = await token.balanceOf(addr1.address);
expect(newBalance).to.equal(initialBalance.add(mintAmount));
});
it("Should allow burning tokens", async function () {
const [owner] = await ethers.getSigners();
const RewardsToken = await ethers.getContractFactory("RewardsToken");
const token = await RewardsToken.deploy();
await token.deployed();
const initialBalance = await token.balanceOf(owner.address);
const burnAmount = ethers.utils.parseEther("100");
await token.burn(burnAmount);
const newBalance = await token.balanceOf(owner.address);
expect(newBalance).to.equal(initialBalance.sub(burnAmount));
});
// New tests for reward functionality
it("Should not allow claiming rewards before interval passes", async function() {
const [owner, user] = await ethers.getSigners();
const RewardsToken = await ethers.getContractFactory("RewardsToken");
const token = await RewardsToken.deploy();
await token.deployed();
// Try to claim rewards immediately
await expect(token.connect(user).claimRewards())
.to.be.revertedWith("RewardsToken: too early to claim rewards");
});
it("Should allow claiming rewards after interval passes", async function() {
const [owner, user] = await ethers.getSigners();
const RewardsToken = await ethers.getContractFactory("RewardsToken");
const token = await RewardsToken.deploy();
await token.deployed();
// Fast forward time by 1 day + 1 second
await time.increase(86401);
// Now claim should work
await expect(token.connect(user).claimRewards())
.to.emit(token, "RewardClaimed")
.withArgs(user.address, ethers.utils.parseEther("100"));
// User should have received tokens
expect(await token.balanceOf(user.address))
.to.equal(ethers.utils.parseEther("100"));
});
it("Should allow owner to change reward rate", async function() {
const [owner] = await ethers.getSigners();
const RewardsToken = await ethers.getContractFactory("RewardsToken");
const token = await RewardsToken.deploy();
await token.deployed();
// Change reward rate to 200
await token.setRewardRate(200);
expect(await token.rewardRate()).to.equal(200);
});
});
Step 2: Run the Tests
axicov test
You should see output similar to:
๐งช Running tests...
โ Contracts compiled successfully!
โ Tests completed!
RewardsToken
โ Should deploy with correct name and symbol
โ Should have the correct initial supply
โ Should allow minting new tokens
โ Should allow burning tokens
โ Should not allow claiming rewards before interval passes
โ Should allow claiming rewards after interval passes
โ Should allow owner to change reward rate
7 passing (3.45s)
โ All tests passed successfully!
Deployment
Step 1: Deploy to Testnet
Ensure your .env file contains your private key, then run:
axicov deploy RewardsToken --network sonicblaze
You'll see output like:
๐ Deploying RewardsToken to sonicblaze...
โ Contract compiled successfully!
โ Contract deployed successfully!
๐ Contract Address: 0x8B45D5A8617E980C9cDAb21c16eDC4C6134AC8D0
๐ Explorer: https://sonicblaze.explorer/address/0x8B45D5A8617E980C9cDAb21c16eDC4C6134AC8D0
โ Deployment info saved to deployments/RewardsToken-sonicblaze.json