- (Review) Design patterns
- Architecture overview
- Lottery structure
- Implement ownable
- Owner start lottery and define betting duration and fee
- Define a block timestamp target
- Players must buy an ERC20 with ETH
- Players pay ERC20 to bet
- Only possible before block timestamp met
- Anyone can roll the lottery
- Only after block timestamp target is met
- Randomness from RANDAO
- Winner receives the pooled ERC20 minus fee
- Owner can withdraw fees and restart lottery
- Players can burn ERC20 tokens and redeem ETH
https://coinsbench.com/how-to-create-a-lottery-smart-contract-with-solidity-4515ff6f849a
- Implementing the relatively safe randomness source from
prevrandao
- Implementing the time lock using block timestamp
- Block time estimation
- Implementing the fee
- (Review) Dealing with decimals
- Withdrawing from pool and redeeming eth
- (Bonus) "Normal" factory pattern and clone pattern overview
Token:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract LotteryToken is ERC20, ERC20Burnable, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
Script reference:
let contract: Lottery;
let token: LotteryToken;
let accounts: SignerWithAddress[];
const BET_PRICE = 1;
const BET_FEE = 0.2;
async function main() {
await initContracts();
await initAccounts();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
mainMenu(rl);
}
async function initContracts() {
// TODO: set up contracts
}
async function initAccounts() {
// TODO: set up accounts
}
async function mainMenu(rl: readline.Interface) {
menuOptions(rl);
}
function menuOptions(rl: readline.Interface) {
rl.question(
"Select operation: \n Options: \n [0]: Exit \n [1]: Check state \n [2]: Open bets \n [3]: Top up account tokens \n [4]: Bet with account \n [5]: Close bets \n [6]: Check player prize \n [7]: Withdraw \n [8]: Burn tokens \n",
async (answer: string) => {
console.log(`Selected: ${answer}\n`);
const option = Number(answer);
switch (option) {
case 0:
rl.close();
return;
case 1:
await checkState();
mainMenu(rl);
break;
case 2:
rl.question("Input duration (in seconds)\n", async (duration) => {
try {
await openBets(duration);
} catch (error) {
console.log("error\n");
console.log({ error });
}
mainMenu(rl);
});
break;
case 3:
rl.question("What account (index) to use?\n", async (index) => {
await displayBalance(index);
rl.question("Buy how many tokens?\n", async (amount) => {
try {
await buyTokens(index, amount);
await displayBalance(index);
await displayTokenBalance(index);
} catch (error) {
console.log("error\n");
console.log({ error });
}
mainMenu(rl);
});
});
break;
case 4:
rl.question("What account (index) to use?\n", async (index) => {
await displayTokenBalance(index);
rl.question("Bet how many times?\n", async (amount) => {
try {
await bet(index, amount);
await displayTokenBalance(index);
} catch (error) {
console.log("error\n");
console.log({ error });
}
mainMenu(rl);
});
});
break;
case 5:
try {
await closeLottery();
} catch (error) {
console.log("error\n");
console.log({ error });
}
mainMenu(rl);
break;
case 6:
rl.question("What account (index) to use?\n", async (index) => {
const prize = await displayPrize(index);
if (Number(prize) > 0) {
rl.question(
"Do you want to claim your prize? [Y/N]\n",
async (answer) => {
if (answer.toLowerCase() === "y") {
try {
await claimPrize(index, prize);
} catch (error) {
console.log("error\n");
console.log({ error });
}
}
mainMenu(rl);
}
);
} else {
mainMenu(rl);
}
});
break;
case 7:
await displayTokenBalance("0");
await displayOwnerPool();
rl.question("Withdraw how many tokens?\n", async (amount) => {
try {
await withdrawTokens(amount);
} catch (error) {
console.log("error\n");
console.log({ error });
}
mainMenu(rl);
});
break;
case 8:
rl.question("What account (index) to use?\n", async (index) => {
await displayTokenBalance(index);
rl.question("Burn how many tokens?\n", async (amount) => {
try {
await burnTokens(index, amount);
} catch (error) {
console.log("error\n");
console.log({ error });
}
mainMenu(rl);
});
});
break;
default:
throw new Error("Invalid option");
}
}
);
}
async function checkState() {
// TODO
}
async function openBets(duration: string) {
// TODO
}
async function displayBalance(index: string) {
// TODO
}
async function buyTokens(index: string, amount: string) {
// TODO
}
async function displayTokenBalance(index: string) {
// TODO
}
async function bet(index: string, amount: string) {
// TODO
}
async function closeLottery() {
// TODO
}
async function displayPrize(index: string) {
// TODO
return "TODO";
}
async function claimPrize(index: string, amount: string) {
// TODO
}
async function displayOwnerPool() {
// TODO
}
async function withdrawTokens(amount: string) {
// TODO
}
async function burnTokens(index: string, amount: string) {
// TODO
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
https://medium.com/coinmonks/math-in-solidity-part-1-numbers-384c8377f26d
https://betterprogramming.pub/learn-solidity-the-factory-pattern-75d11c3e7d29
https://blog.logrocket.com/cloning-solidity-smart-contracts-factory-pattern/