It's easy; to prevent copy-paste, you can read it in this article.
Let's imagine a vending machine, and we try to compare smart contract behavior with it:
But today we are going to make a vending machine. I thought for a long time (about 5 minutes) what to do, so exciting and vital, and realized that humanity has nothing more essential than the opportunity to invest a dollar and break the bank in the form of millions.
Unfortunately, not everyone will win, but a certain number of participants, for example, 10 (or less if fewer people are willing)
Initialise the project and all other stuff
mkdir contest
cd contest
npm init -y
npm install --save-dev hardhat
npx hardhat init
In the contracts
directory, create a new file named Contest.sol
:
pragma solidity ^0.8.9;
contract Contest {
event ContestClosed(address contest);
event ContestWinner(address winner, uint256 ticket, uint256 amount);
event ContestParticipate(address winner, uint amount);
enum Status {
OPEN,
CLOSED
}
constructor(uint _totalWinners, string memory _name, uint _endTime, uint256 _ticketPrice ) {
}
function participate() public payable {
}
function getWinners() public {
}
function getWinnersLength() public view returns (uint256) {
}
}
This is skeleton for contract. Let’s dive into it
contract Contest {
...
constructor(uint _totalWinners, string memory _name, uint _endTime, uint256 _ticketPrice ) {
}
...
}
Constructor for contract, that describes the basic rules of our contract, such as
contract Contest {
...
function participate() public payable {
}
...
}
participate function, where if you pay enough, take a ticket that gives you a chance to get a prize, marked as public (available outside contract) and payable, that indicates that this function can receive Ethers. When calling this function, the caller can send along some Ether.
contract Contest {
...
function getWinners() public {
}
...
}
choose winners, marked only as public and in code later we made that call it can only contract creator
contract Contest {
...
function getWinnersLength() public view returns (uint256) {
}
}
simple function that count total winners for our contest
contract Contest {
event ContestClosed(address contest);
event ContestWinner(address winner, uint256 ticket, uint256 amount);
event ContestParticipate(address winner, uint amount);
...
}
Here is few events for our functions. Events is like announcements from a smart contract. When something important happens in the contract, it "shouts out" using an event, and anyone interested can listen to that shout and know about it.
contract Contest {
...
enum Status {
OPEN,
CLOSED
}
...
}
Enum which represent status out contract.
pragma solidity ^0.8.9;
contract Contest {
...
address public admin;
string public name;
uint public endTime;
uint public totalWinners;
uint256 public ticketPrice;
uint counter;
mapping(address => uint) public balance;
mapping(address => uint[]) public ownedTickets;
mapping(uint => address) public tickets;
uint[] ticketKeys;
uint[] public winners;
bytes32 seed;
Status public status = Status.OPEN;
...
constructor(uint _totalWinners, string memory _name, uint _endTime, uint256 _ticketPrice ) {
admin = msg.sender;
name = _name;
endTime = _endTime;
totalWinners = _totalWinners;
ticketPrice = _ticketPrice;
}
...
}
We little bit enrich our constructor. At first we store initial data in variables for next using.
Also we add few mappings
And arrays which will be used later
And variables
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "hardhat/console.sol";
contract Contest {
...
function participate() public payable {
require(status == Status.OPEN, "Contest closed");
require(endTime == 0 || block.timestamp < endTime, "Contest closed");
require(msg.value >= ticketPrice, "Not enough ether to purchase Ticket.");
uint ticketId = counter;
ownedTickets[msg.sender].push(ticketId);
balance[msg.sender] += 1;
tickets[ticketId] = msg.sender;
ticketKeys.push(ticketId);
seed = keccak256(
abi.encodePacked(
seed,
block.timestamp,
block.prevrandao,
msg.sender
)
);
counter += 1;
emit ContestParticipate(msg.sender, ticketId);
}
...
}
in this function at first we check required conditions, contest should be opened, current time less than max time and user send enough ethers for participate.
After all conditions passed, we get current counter number which will be our ticketId and associate it with current user, increase users balance, update seed and send event that user participated
Why we update seed on every time than someone participate?
Good question! Solidity doesn't have random function and one of best practice for it - make hash from string. And in order to reduce the dependence on external parameters and reduce the likelihood that this phrase can be calculated for the next steps, we update it every time through pseudo-random parameters.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "hardhat/console.sol";
contract Contest {
...
function getWinners() public {
require(msg.sender == admin, "Only admin node can get winners.");
require(status == Status.OPEN, "Contest closed");
uint total = totalWinners < counter ? totalWinners : counter;
if (total == 0) {
status = Status.CLOSED;
emit ContestClosed(address(this));
return;
}
uint256 amount = address(this).balance / total;
for (uint i = 0; i < total; i++) {
getWinner(amount);
}
status = Status.CLOSED;
emit ContestClosed(address(this));
}
function getWinner(uint256 amount) private {
seed = keccak256(
abi.encodePacked(
seed,
block.timestamp,
block.prevrandao,
msg.sender
)
);
uint256 winnerKey = uint(seed) % ticketKeys.length;
uint winnerTicket = ticketKeys[winnerKey];
address to = tickets[winnerTicket];
ticketKeys[winnerKey] = ticketKeys[ticketKeys.length - 1];
ticketKeys.pop();
winners.push(winnerTicket);
payable(to).transfer(amount);
emit ContestWinner(to, winnerTicket, amount);
}
...
}
And finally, the selection of winners. I moved the method for selecting a specific winner into a separate private function.
Let's consider the getWinners
function. Everything is as simple as possible here, we check access rights and that the draw is not closed yet and find the actual number of winners. We calculate the size of the prize for each participant and select the winner from the list of tickets.
Once we have found all the winners, we mark the giveaway closed and send the corresponding event
The getWinner
function is a little more interesting. For each winner, we need to find a new number, which we divide by the number of tickets. This is how we get the index of the winning ticket.
Having received the index, we find the ticket and there is already a winner based on it.
To prevent the same ticket from entering twice, we replace it with the last ticket and remove the last ticket from the list of winners. Well, at the same time we send the winner the winnings due to him and create an event for this
import { ethers } from "hardhat";
import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
const CONTEST_NAME = "test name";
const AMOUNT = ethers.utils.parseEther("1");
describe("Contest", function () {
async function deployOneYearLockFixture() {
const endTime = (await time.latest()) + 60;
const [owner, participant1, participant2, participant3, participant4, participant5 ] = await ethers.getSigners();
const ticketPrise = ethers.utils.parseEther("1");
const Contest = await ethers.getContractFactory("Contest");
const contest = await Contest.deploy(10, CONTEST_NAME, endTime, ticketPrise);
return { contest, endTime, owner, participant1, participant2, participant3, participant4, participant5, };
}
it("should get name and description", async () => {
const { contest, endTime } = await loadFixture(deployOneYearLockFixture);
const balance0ETH = await contest.provider.getBalance(contest.address);
expect(await contest.name()).to.be.equal("test name");
expect(await contest.endTime()).to.be.equal(endTime);
expect(balance0ETH.toHexString()).to.be.equal("0x00");
});
it("should provide to participate", async () => {
const { contest, participant1, participant2 } = await loadFixture(deployOneYearLockFixture);
expect(
await contest.connect(participant1).participate({value: AMOUNT})
).to.be.exist;
expect(
await contest.connect(participant1).participate({value: AMOUNT})
).to.be.exist;
expect(
await contest.connect(participant2).participate({value: AMOUNT})
).to.be.exist;
expect(await contest.balance(participant1.getAddress())).to.be.equal(2);
expect(await contest.balance(participant2.getAddress())).to.be.equal(1);
const participant1Balance = await contest.provider.getBalance(participant1.getAddress());
const participant2Balance = await contest.provider.getBalance(participant2.getAddress());
await contest.getWinners();
expect(await contest.getWinnersLength()).to.be.equal(3);
const participant1BalanceAfter = await contest.provider.getBalance(participant1.getAddress());
expect(participant1BalanceAfter.toHexString()).to.be.equal(participant1Balance.add(AMOUNT.mul(2)).toHexString());
const participant2BalanceAfter = await contest.provider.getBalance(participant2.getAddress());
expect(participant2BalanceAfter.toHexString()).to.be.equal(participant2Balance.add(AMOUNT).toHexString());
});
it("should select winners", async () => {
const {
contest,
participant1,
participant2,
participant3,
participant4,
participant5,
} = await loadFixture(deployOneYearLockFixture);
for (let i = 0; i < 3; i += 1) {
await contest.connect(participant1).participate({value: ethers.utils.parseEther("1")});
await contest.connect(participant2).participate({value: ethers.utils.parseEther("1")});
await contest.connect(participant3).participate({value: ethers.utils.parseEther("1")});
await contest.connect(participant4).participate({value: ethers.utils.parseEther("1")});
await contest.connect(participant5).participate({value: ethers.utils.parseEther("1")});
}
expect(await contest.balance(participant1.getAddress())).to.be.equal(3);
expect(await contest.balance(participant2.getAddress())).to.be.equal(3);
expect(await contest.balance(participant3.getAddress())).to.be.equal(3);
expect(await contest.balance(participant4.getAddress())).to.be.equal(3);
expect(await contest.balance(participant5.getAddress())).to.be.equal(3);
expect(await contest.getWinners()).to.be.exist;
expect(await contest.getWinnersLength()).to.be.equal(10);
});
it("should not apply participant if date is end", async () => {
const endTime = (await time.latest()) - 60;
const [owner, participant1] = await ethers.getSigners();
const Contest = await ethers.getContractFactory("Contest");
const contest = await Contest.deploy(10, CONTEST_NAME, endTime, ethers.utils.parseEther("1"));
await expect(
contest.connect(participant1).participate({value: ethers.utils.parseEther("1")})
).to.be.revertedWith("Contest closed");
});
it("should apply participant if date is 0", async () => {
const [owner, participant1] = await ethers.getSigners();
const Contest = await ethers.getContractFactory("Contest");
const contest = await Contest.deploy( 10, CONTEST_NAME, 0, ethers.utils.parseEther("1"));
await contest.connect(participant1).participate({value: ethers.utils.parseEther("1")});
expect(await contest.balance(participant1.getAddress())).to.be.equal(1);
});
});
These are tests, I don’t know what else to describe) Before each new iteration, we deploy a new contract and check all cases
In this article, I tried to collect my experience in developing a smart contract (a slightly more advanced version is used in production). Next time we will make NFTs that can be added via Telegram and consider IPFS. The source code for this article is available at https://github.com/vivalaakam/contest
Thank you for your time =)