├── .solhintignore ├── .npmignore ├── .eslintignore ├── thumbnails ├── ecdsa.png ├── proxy.png ├── rentable-nft.png ├── solidity-create2.png ├── merkle-tree-airdrop.png ├── hardhat-best-practices.png └── perfect-solidity-stack.png ├── .gitignore ├── contracts ├── create │ ├── CreateCat.sol │ ├── CreateCatFactory.sol │ └── Create2CatFactory.sol ├── merkle-airdrop │ ├── MerkleAirdropToken.sol │ └── MerkleAirdrop.sol ├── intro │ └── Greeter.sol ├── cat-token-proxy │ ├── LegacyCatToken.sol │ └── ModernCatToken.sol ├── signature-checker │ └── SignatureChecker.sol └── rentable-nft │ └── RentableNFT.sol ├── tsconfig.json ├── .solhint.json ├── scripts └── intro │ └── Greeter.ts ├── hardhat.config.ts ├── test ├── intro │ └── Greeter.test.ts ├── signature-checker │ └── SignatureChecker.test.ts ├── cat-token-proxy │ └── CatTokenProxy.test.ts ├── create │ └── Cat.test.ts ├── merkle-airdrop │ └── MerkleAirdrop.test.ts └── rentable-nft │ └── RentableNFT.test.ts ├── .eslintrc.js ├── package.json └── README.md /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | hardhat.config.ts 2 | scripts 3 | test 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage 5 | -------------------------------------------------------------------------------- /thumbnails/ecdsa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modagavr/sol-like-a-pro/HEAD/thumbnails/ecdsa.png -------------------------------------------------------------------------------- /thumbnails/proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modagavr/sol-like-a-pro/HEAD/thumbnails/proxy.png -------------------------------------------------------------------------------- /thumbnails/rentable-nft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modagavr/sol-like-a-pro/HEAD/thumbnails/rentable-nft.png -------------------------------------------------------------------------------- /thumbnails/solidity-create2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modagavr/sol-like-a-pro/HEAD/thumbnails/solidity-create2.png -------------------------------------------------------------------------------- /thumbnails/merkle-tree-airdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modagavr/sol-like-a-pro/HEAD/thumbnails/merkle-tree-airdrop.png -------------------------------------------------------------------------------- /thumbnails/hardhat-best-practices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modagavr/sol-like-a-pro/HEAD/thumbnails/hardhat-best-practices.png -------------------------------------------------------------------------------- /thumbnails/perfect-solidity-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modagavr/sol-like-a-pro/HEAD/thumbnails/perfect-solidity-stack.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain-types 6 | 7 | #Hardhat files 8 | cache 9 | artifacts 10 | .DS_Store 11 | 12 | .openzeppelin/unknown-31337.json -------------------------------------------------------------------------------- /contracts/create/CreateCat.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | contract CreateCat { 5 | uint8 public immutable age; 6 | 7 | constructor(uint8 _age) { 8 | age = _age; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/create/CreateCatFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "./CreateCat.sol"; 5 | 6 | contract CreateCatFactory { 7 | address public cat; 8 | 9 | function deployCat(uint8 _age) external { 10 | cat = address(new CreateCat(_age)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/merkle-airdrop/MerkleAirdropToken.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract MerkleAirdropToken is ERC20 { 7 | constructor() ERC20("MerkleAirdropToken", "MAT") { 8 | _mint(msg.sender, 10 * 10**decimals()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true, 9 | }, 10 | "include": [ 11 | "./scripts", 12 | "./test", 13 | "./typechain-types" 14 | ], 15 | "files": [ 16 | "./hardhat.config.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": [ 5 | "error", 6 | "^0.8.0" 7 | ], 8 | "func-visibility": [ 9 | "warn", 10 | { 11 | "ignoreConstructors": true 12 | } 13 | ], 14 | "no-empty-blocks": [ 15 | "off" 16 | ], 17 | "reason-string": [ 18 | "off" 19 | ], 20 | "not-rely-on-time": [ 21 | "off" 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /contracts/intro/Greeter.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | contract Greeter { 5 | string private greeting; 6 | 7 | constructor(string memory _greeting) { 8 | greeting = _greeting; 9 | } 10 | 11 | function greet() public view returns (string memory) { 12 | return greeting; 13 | } 14 | 15 | function setGreeting(string memory _greeting) public { 16 | greeting = _greeting; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/intro/Greeter.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat' 2 | 3 | import { Greeter__factory } from '../../typechain-types' 4 | 5 | async function main() { 6 | const signers = await ethers.getSigners() 7 | 8 | const greeter = await new Greeter__factory(signers[0]).deploy( 9 | 'Hello, Hardhat!' 10 | ) 11 | 12 | await greeter.deployed() 13 | 14 | console.log('Greeter deployed to:', greeter.address) 15 | } 16 | 17 | main().catch((error) => { 18 | console.error(error) 19 | process.exitCode = 1 20 | }) 21 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-etherscan' 2 | import '@nomiclabs/hardhat-waffle' 3 | import '@openzeppelin/hardhat-upgrades' 4 | import '@typechain/hardhat' 5 | import 'hardhat-gas-reporter' 6 | import 'solidity-coverage' 7 | 8 | import { HardhatUserConfig } from 'hardhat/config' 9 | 10 | const config: HardhatUserConfig = { 11 | solidity: { 12 | version: '0.8.15', 13 | settings: { 14 | optimizer: { 15 | enabled: true, 16 | runs: 200 17 | } 18 | } 19 | }, 20 | gasReporter: { 21 | enabled: true, 22 | currency: 'USD' 23 | } 24 | } 25 | 26 | export default config 27 | -------------------------------------------------------------------------------- /test/intro/Greeter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { ethers } from 'hardhat' 3 | 4 | import { Greeter__factory } from '../../typechain-types' 5 | 6 | describe('Greeter', () => { 7 | it("Should return the new greeting once it's changed", async () => { 8 | const signers = await ethers.getSigners() 9 | 10 | const greeter = await new Greeter__factory(signers[0]).deploy( 11 | 'Hello, world!' 12 | ) 13 | 14 | expect(await greeter.greet()).to.eq('Hello, world!') 15 | 16 | await greeter.setGreeting('Hola, mundo!') 17 | 18 | expect(await greeter.greet()).to.equal('Hola, mundo!') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /contracts/cat-token-proxy/LegacyCatToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 7 | 8 | contract LegacyCatToken is Initializable, ERC20Upgradeable, OwnableUpgradeable { 9 | /// @custom:oz-upgrades-unsafe-allow constructor 10 | constructor() initializer {} 11 | 12 | function initialize() public initializer { 13 | __ERC20_init("CatToken", "CAT"); 14 | __Ownable_init(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true 7 | }, 8 | plugins: ['@typescript-eslint', 'simple-import-sort', 'prettier'], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:prettier/recommended' 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaVersion: 12 17 | }, 18 | rules: { 19 | 'prettier/prettier': [ 20 | 'warn', 21 | { 22 | semi: false, 23 | singleQuote: true, 24 | trailingComma: 'none' 25 | } 26 | ], 27 | 'simple-import-sort/imports': 'warn', 28 | 'simple-import-sort/exports': 'warn' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/cat-token-proxy/ModernCatToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 7 | 8 | contract ModernCatToken is Initializable, ERC20Upgradeable, OwnableUpgradeable { 9 | /// @custom:oz-upgrades-unsafe-allow constructor 10 | constructor() initializer {} 11 | 12 | function initialize() public initializer { 13 | __ERC20_init("CatToken", "CAT"); 14 | __Ownable_init(); 15 | } 16 | 17 | function mint(address to, uint256 amount) public onlyOwner { 18 | _mint(to, amount); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/signature-checker/SignatureChecker.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 5 | 6 | contract SignatureChecker { 7 | using ECDSA for bytes32; 8 | 9 | bytes32 public constant CAT = keccak256("Cat"); 10 | 11 | address public immutable claimer; 12 | 13 | uint8 public giftsClaimed = 0; 14 | 15 | constructor() { 16 | claimer = msg.sender; 17 | } 18 | 19 | function isValidSignature(bytes calldata signature) 20 | public 21 | view 22 | returns (bool) 23 | { 24 | return CAT.toEthSignedMessageHash().recover(signature) == claimer; 25 | } 26 | 27 | function claimGift(bytes calldata signature) external { 28 | require( 29 | isValidSignature(signature), 30 | "SignatureChecker: Invalid Signature" 31 | ); 32 | 33 | giftsClaimed++; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/create/Create2CatFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts/utils/Create2.sol"; 5 | 6 | import "./CreateCat.sol"; 7 | 8 | contract Create2CatFactory { 9 | address public cat; 10 | 11 | function deployCat(bytes32 _salt, uint8 _age) external returns (bool) { 12 | cat = Create2.deploy( 13 | 0, 14 | _salt, 15 | abi.encodePacked(type(CreateCat).creationCode, abi.encode(_age)) 16 | ); 17 | 18 | return true; 19 | } 20 | 21 | function computeCatAddress(bytes32 _salt, uint8 _age) 22 | public 23 | view 24 | returns (address) 25 | { 26 | return 27 | Create2.computeAddress( 28 | _salt, 29 | keccak256( 30 | abi.encodePacked( 31 | type(CreateCat).creationCode, 32 | abi.encode(_age) 33 | ) 34 | ) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/signature-checker/SignatureChecker.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { arrayify, id } from 'ethers/lib/utils' 3 | import { ethers } from 'hardhat' 4 | 5 | import { SignatureChecker__factory } from '../../typechain-types' 6 | 7 | describe('Signature Checker', () => { 8 | it('Full Cycle', async () => { 9 | const [claimer, friend] = await ethers.getSigners() 10 | 11 | const signatureChecker = await new SignatureChecker__factory( 12 | claimer 13 | ).deploy() 14 | 15 | const catHash = await signatureChecker.CAT() 16 | 17 | expect(catHash).to.eq(id('Cat')) 18 | 19 | expect(await signatureChecker.claimer()).to.eq(claimer.address) 20 | 21 | const claimerSignature = await claimer.signMessage(arrayify(catHash)) 22 | 23 | expect(await signatureChecker.isValidSignature(claimerSignature)).to.eq( 24 | true 25 | ) 26 | 27 | const friendSignature = await friend.signMessage(arrayify(catHash)) 28 | 29 | expect(await signatureChecker.isValidSignature(friendSignature)).to.eq( 30 | false 31 | ) 32 | 33 | expect(await signatureChecker.giftsClaimed()).to.eq(0) 34 | 35 | await signatureChecker.claimGift(claimerSignature) 36 | 37 | expect(await signatureChecker.giftsClaimed()).to.eq(1) 38 | 39 | await expect( 40 | signatureChecker.claimGift(friendSignature) 41 | ).to.be.revertedWith('SignatureChecker: Invalid Signature') 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/cat-token-proxy/CatTokenProxy.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { constants } from 'ethers' 3 | import { parseEther } from 'ethers/lib/utils' 4 | import { ethers, upgrades } from 'hardhat' 5 | 6 | import { LegacyCatToken, ModernCatToken } from '../../typechain-types' 7 | 8 | describe('Cat Token Proxy', () => { 9 | it('Full Cycle', async () => { 10 | const [signer] = await ethers.getSigners() 11 | 12 | const LegacyCatTokenFactory = await ethers.getContractFactory( 13 | 'LegacyCatToken' 14 | ) 15 | 16 | const legacyCatToken = (await upgrades.deployProxy( 17 | LegacyCatTokenFactory 18 | )) as LegacyCatToken 19 | 20 | await legacyCatToken.deployed() 21 | 22 | expect(await legacyCatToken.totalSupply()).to.eql(constants.Zero) 23 | 24 | const ModernCatTokenFactory = await ethers.getContractFactory( 25 | 'ModernCatToken' 26 | ) 27 | 28 | const modernCatToken = (await upgrades.upgradeProxy( 29 | legacyCatToken.address, 30 | ModernCatTokenFactory 31 | )) as ModernCatToken 32 | 33 | expect(modernCatToken.address).to.eq(legacyCatToken.address) 34 | 35 | expect(await modernCatToken.totalSupply()).to.eql(constants.Zero) 36 | 37 | await modernCatToken.mint(signer.address, parseEther('1')) 38 | 39 | expect(await modernCatToken.totalSupply()) 40 | .to.eql(await modernCatToken.balanceOf(signer.address)) 41 | .to.eql(parseEther('1')) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /contracts/merkle-airdrop/MerkleAirdrop.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | 8 | contract MerkleAirdrop { 9 | using SafeERC20 for IERC20; 10 | 11 | address public immutable token; 12 | bytes32 public immutable merkleRoot; 13 | 14 | mapping(address => bool) public claimed; 15 | 16 | event Claim(address indexed claimer); 17 | 18 | constructor(address _token, bytes32 _merkleRoot) { 19 | token = _token; 20 | merkleRoot = _merkleRoot; 21 | } 22 | 23 | function claim(bytes32[] calldata merkleProof) external { 24 | require( 25 | canClaim(msg.sender, merkleProof), 26 | "MerkleAirdrop: Address is not a candidate for claim" 27 | ); 28 | 29 | claimed[msg.sender] = true; 30 | 31 | IERC20(token).safeTransfer(msg.sender, 1 ether); 32 | 33 | emit Claim(msg.sender); 34 | } 35 | 36 | function canClaim(address claimer, bytes32[] calldata merkleProof) 37 | public 38 | view 39 | returns (bool) 40 | { 41 | return 42 | !claimed[claimer] && 43 | MerkleProof.verify( 44 | merkleProof, 45 | merkleRoot, 46 | keccak256(abi.encodePacked(claimer)) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sol-like-a-pro", 3 | "description": "Code in Solidity like a PRO with Egor Gavrilov", 4 | "author": "Egor Gavrilov", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "hardhat compile", 8 | "test": "hardhat test" 9 | }, 10 | "devDependencies": { 11 | "@ethersproject/providers": "^5.6.8", 12 | "@nomiclabs/hardhat-ethers": "^2.1.0", 13 | "@nomiclabs/hardhat-etherscan": "^3.1.0", 14 | "@nomiclabs/hardhat-waffle": "^2.0.3", 15 | "@openzeppelin/contracts": "^4.7.2", 16 | "@openzeppelin/contracts-upgradeable": "^4.7.2", 17 | "@openzeppelin/hardhat-upgrades": "^1.19.1", 18 | "@typechain/ethers-v5": "^10.1.0", 19 | "@typechain/hardhat": "^6.1.2", 20 | "@types/chai": "^4.3.1", 21 | "@types/mocha": "^9.1.1", 22 | "@types/node": "^18.6.3", 23 | "@typescript-eslint/eslint-plugin": "^5.31.0", 24 | "@typescript-eslint/parser": "^5.31.0", 25 | "chai": "^4.3.6", 26 | "dayjs": "^1.11.4", 27 | "dotenv": "^16.0.1", 28 | "eslint": "^8.20.0", 29 | "eslint-config-prettier": "^8.5.0", 30 | "eslint-plugin-prettier": "^4.2.1", 31 | "eslint-plugin-simple-import-sort": "^7.0.0", 32 | "ethereum-waffle": "^3.4.4", 33 | "ethers": "^5.6.9", 34 | "hardhat": "^2.10.1", 35 | "hardhat-gas-reporter": "^1.0.8", 36 | "keccak256": "^1.0.6", 37 | "merkletreejs": "^0.2.32", 38 | "prettier": "^2.7.1", 39 | "solhint": "^3.3.7", 40 | "solidity-coverage": "^0.7.21", 41 | "ts-node": "^10.9.1", 42 | "typechain": "^8.1.0", 43 | "typescript": "^4.7.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code in Solidity like a PRO with Egor Gavrilov 2 | 3 | ## Check out YouTube channel [youtube.com/EgorGavrilov](https://youtube.com/EgorGavrilov) 4 | 5 | ### Tutorials 6 | 7 | 1. [Perfect Solidity Stack](https://youtu.be/NohOI4HWYCs) 8 | 9 | It's easy to get started with Solidity programming, but how do you approach it professionally? We figure it out in this video. 10 | 11 | ![PerfectSolidityStack](thumbnails/perfect-solidity-stack.png) 12 | 13 | 2. [Predict Contract Address](https://youtu.be/A27p0qfpcAc) 14 | 15 | Deploy your Smart Contract with a precomputed address. We will write a Solidity Smart Contract with two options: using Create and Create2. 16 | 17 | ![SolidityCreate2](thumbnails/solidity-create2.png) 18 | 19 | 3. [Rentable NFT](https://youtu.be/DG2lNEMI1TQ) 20 | 21 | Rentable NFTs are the next big trend. Rent out an expensive NFT for one day and take advantage of all the benefits for a small fee. 22 | 23 | ![RentableNFT](thumbnails/rentable-nft.png) 24 | 25 | 4. [Merkle Tree Airdrop](https://youtu.be/XhzkwB71IJE) 26 | 27 | Use Merkle tree to airdrop your token in a cheap, elegant and efficient way. 28 | 29 | ![MerkleTreeAirdrop](thumbnails/merkle-tree-airdrop.png) 30 | 31 | 5. [Digital Signatures](https://youtu.be/w9St9aU9UqQ) 32 | 33 | Elliptic Curve Digital Signature Algorithm is a popular method of digital identification. 34 | 35 | ![Digital Signatures](thumbnails/ecdsa.png) 36 | 37 | 6. [Upgradeable Smart Contracts](https://youtu.be/3XXw-cIWgV4) 38 | 39 | Upgrade your Smart Contracts using Transparent Upgradeable Proxy. 40 | 41 | ![Upgradeable Smart Contracts](thumbnails/proxy.png) 42 | 43 | 7. [10 Hardhat Best Practices](https://youtu.be/hMmDCczYBs4) 44 | 45 | Hardhat is an Ethereum Development Environment for Professionals. We will take a look at 10 Hardhat Best Practices. 46 | 47 | ![10 Hardhat Best Practices](thumbnails/hardhat-best-practices.png) 48 | -------------------------------------------------------------------------------- /test/create/Cat.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' 2 | import { expect } from 'chai' 3 | import { constants } from 'ethers' 4 | import { getContractAddress, id } from 'ethers/lib/utils' 5 | import { ethers } from 'hardhat' 6 | 7 | import { 8 | Create2CatFactory__factory, 9 | CreateCat__factory, 10 | CreateCatFactory__factory 11 | } from '../../typechain-types' 12 | 13 | describe('Create Cat', () => { 14 | let signer: SignerWithAddress 15 | 16 | beforeEach(async () => { 17 | signer = (await ethers.getSigners())[0] 18 | }) 19 | 20 | it('Using Create', async () => { 21 | const factory = await new CreateCatFactory__factory(signer).deploy() 22 | 23 | expect(await factory.cat()).to.eq(constants.AddressZero) 24 | 25 | const precomputedCatAddress = getContractAddress({ 26 | from: factory.address, 27 | nonce: 1 28 | }) 29 | 30 | await factory.deployCat(2) 31 | 32 | expect(await factory.cat()).to.eq(precomputedCatAddress) 33 | 34 | expect( 35 | await CreateCat__factory.connect(precomputedCatAddress, signer).age() 36 | ).to.eq(2) 37 | 38 | for (const nonce of [2, 3, 4, 5]) { 39 | await factory.deployCat(3) 40 | 41 | expect(await factory.cat()).to.eq( 42 | getContractAddress({ 43 | from: factory.address, 44 | nonce 45 | }) 46 | ) 47 | } 48 | }) 49 | 50 | it('Using Create2', async () => { 51 | const factory = await new Create2CatFactory__factory(signer).deploy() 52 | 53 | expect(await factory.cat()).to.eq(constants.AddressZero) 54 | 55 | const precomputedCatAddress = await factory.computeCatAddress( 56 | id('🐱Tom'), 57 | 5 58 | ) 59 | 60 | await factory.deployCat(id('🐱Tom'), 5) 61 | 62 | expect(await factory.cat()).to.eq(precomputedCatAddress) 63 | 64 | expect( 65 | await CreateCat__factory.connect(precomputedCatAddress, signer).age() 66 | ).to.eq(5) 67 | 68 | await factory.deployCat(id('🍯Honey'), 7) 69 | await factory.deployCat(id('🐱Tom'), 7) 70 | 71 | await expect(factory.deployCat(id('🐱Tom'), 5)).to.be.revertedWith( 72 | 'Create2: Failed on deploy' 73 | ) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/merkle-airdrop/MerkleAirdrop.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { randomBytes } from 'crypto' 3 | import { Wallet } from 'ethers' 4 | import { parseEther } from 'ethers/lib/utils' 5 | import { ethers } from 'hardhat' 6 | import keccak256 from 'keccak256' 7 | import { MerkleTree } from 'merkletreejs' 8 | 9 | import { 10 | MerkleAirdrop__factory, 11 | MerkleAirdropToken__factory 12 | } from '../../typechain-types' 13 | 14 | describe('Merkle Airdrop', () => { 15 | it('Full Cycle', async () => { 16 | const [signer, guy] = await ethers.getSigners() 17 | 18 | const token = await new MerkleAirdropToken__factory(signer).deploy() 19 | 20 | const randomAddresses = new Array(15) 21 | .fill(0) 22 | .map(() => new Wallet(randomBytes(32).toString('hex')).address) 23 | 24 | const merkleTree = new MerkleTree( 25 | randomAddresses.concat(signer.address), 26 | keccak256, 27 | { hashLeaves: true, sortPairs: true } 28 | ) 29 | 30 | const root = merkleTree.getHexRoot() 31 | 32 | const airdrop = await new MerkleAirdrop__factory(signer).deploy( 33 | token.address, 34 | root 35 | ) 36 | 37 | await token.transfer(airdrop.address, parseEther('10')) 38 | 39 | const proof = merkleTree.getHexProof(keccak256(signer.address)) 40 | 41 | expect(await airdrop.claimed(signer.address)).to.eq(false) 42 | 43 | expect(await airdrop.canClaim(signer.address, proof)).to.eq(true) 44 | 45 | Object.assign(airdrop, { getAddress: () => airdrop.address }) 46 | 47 | await expect(() => airdrop.claim(proof)).to.changeTokenBalances( 48 | token, 49 | [airdrop, signer], 50 | [parseEther('-1'), parseEther('1')] 51 | ) 52 | 53 | expect(await airdrop.claimed(signer.address)).to.eq(true) 54 | 55 | expect(await airdrop.canClaim(signer.address, proof)).to.eq(false) 56 | 57 | await expect(airdrop.claim(proof)).to.be.revertedWith( 58 | 'MerkleAirdrop: Address is not a candidate for claim' 59 | ) 60 | 61 | expect(await airdrop.claimed(guy.address)).to.eq(false) 62 | 63 | expect(await airdrop.canClaim(guy.address, proof)).to.eq(false) 64 | 65 | await expect(airdrop.connect(guy).claim(proof)).to.be.revertedWith( 66 | 'MerkleAirdrop: Address is not a candidate for claim' 67 | ) 68 | 69 | const badProof = merkleTree.getHexProof(keccak256(guy.address)) 70 | 71 | expect(badProof).to.eql([]) 72 | 73 | expect(await airdrop.canClaim(guy.address, badProof)).to.eq(false) 74 | 75 | await expect(airdrop.connect(guy).claim(badProof)).to.be.revertedWith( 76 | 'MerkleAirdrop: Address is not a candidate for claim' 77 | ) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /contracts/rentable-nft/RentableNFT.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.15; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "@openzeppelin/contracts/utils/Counters.sol"; 8 | 9 | contract RentableNFT is ERC721, ERC721Enumerable, Ownable { 10 | using Counters for Counters.Counter; 11 | 12 | Counters.Counter private _tokenIdCounter; 13 | 14 | struct Rental { 15 | bool isActive; 16 | address lord; 17 | address renter; 18 | uint256 expiresAt; 19 | } 20 | 21 | mapping(uint256 => Rental) public rental; 22 | 23 | event Rented( 24 | uint256 indexed tokenId, 25 | address indexed lord, 26 | address indexed renter, 27 | uint256 expiresAt 28 | ); 29 | 30 | event FinishedRent( 31 | uint256 indexed tokenId, 32 | address indexed lord, 33 | address indexed renter, 34 | uint256 expiresAt 35 | ); 36 | 37 | constructor() ERC721("RentableNFT", "RENT") {} 38 | 39 | function safeMint(address to) public onlyOwner { 40 | uint256 tokenId = _tokenIdCounter.current(); 41 | _tokenIdCounter.increment(); 42 | _safeMint(to, tokenId); 43 | } 44 | 45 | function rentOut( 46 | address renter, 47 | uint256 tokenId, 48 | uint256 expiresAt 49 | ) external { 50 | _transfer(msg.sender, renter, tokenId); 51 | 52 | rental[tokenId] = Rental({ 53 | isActive: true, 54 | lord: msg.sender, 55 | renter: renter, 56 | expiresAt: expiresAt 57 | }); 58 | 59 | emit Rented(tokenId, msg.sender, renter, expiresAt); 60 | } 61 | 62 | function finishRenting(uint256 tokenId) external { 63 | Rental memory _rental = rental[tokenId]; 64 | 65 | require( 66 | _rental.isActive && 67 | (msg.sender == _rental.renter || 68 | block.timestamp >= _rental.expiresAt), 69 | "RentableNFT: this rental can't be finished" 70 | ); 71 | 72 | delete rental[tokenId]; 73 | 74 | _transfer(_rental.renter, _rental.lord, tokenId); 75 | 76 | emit FinishedRent( 77 | tokenId, 78 | _rental.lord, 79 | _rental.renter, 80 | _rental.expiresAt 81 | ); 82 | } 83 | 84 | function _beforeTokenTransfer( 85 | address from, 86 | address to, 87 | uint256 tokenId 88 | ) internal override(ERC721, ERC721Enumerable) { 89 | require(!rental[tokenId].isActive, "RentableNFT: this token is rented"); 90 | 91 | super._beforeTokenTransfer(from, to, tokenId); 92 | } 93 | 94 | function supportsInterface(bytes4 interfaceId) 95 | public 96 | view 97 | override(ERC721, ERC721Enumerable) 98 | returns (bool) 99 | { 100 | return super.supportsInterface(interfaceId); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/rentable-nft/RentableNFT.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' 2 | import { expect } from 'chai' 3 | import dayjs from 'dayjs' 4 | import { BigNumber, constants } from 'ethers' 5 | import { ethers, network } from 'hardhat' 6 | 7 | import { RentableNFT, RentableNFT__factory } from '../../typechain-types' 8 | 9 | describe('Rentable NFT', () => { 10 | let owner: SignerWithAddress, 11 | lord: SignerWithAddress, 12 | renter: SignerWithAddress, 13 | guy: SignerWithAddress 14 | 15 | let token: RentableNFT 16 | 17 | beforeEach(async () => { 18 | const signers = await ethers.getSigners() 19 | 20 | owner = signers[0] 21 | 22 | lord = signers[1] 23 | 24 | renter = signers[2] 25 | 26 | guy = signers[3] 27 | 28 | token = await new RentableNFT__factory(owner).deploy() 29 | }) 30 | 31 | it('Mint & Transfer', async () => { 32 | expect(await token.totalSupply()).to.eql(constants.Zero) 33 | 34 | await token.safeMint(lord.address) 35 | 36 | expect(await token.totalSupply()) 37 | .to.eql(await token.balanceOf(lord.address)) 38 | .to.eql(constants.One) 39 | 40 | expect( 41 | await token.connect(lord).transferFrom(lord.address, renter.address, 0) 42 | ) 43 | .to.emit(token, 'Transfer') 44 | .withArgs(lord.address, renter.address, 0) 45 | 46 | expect(await token.totalSupply()) 47 | .to.eql(await token.balanceOf(renter.address)) 48 | .to.eql(constants.One) 49 | 50 | expect(await token.balanceOf(lord.address)).to.eql(constants.Zero) 51 | }) 52 | 53 | describe('Rent Out & Finish Renting', () => { 54 | const expiresAt = dayjs().add(1, 'day').unix() 55 | 56 | beforeEach(async () => { 57 | await token.safeMint(lord.address) 58 | await token.safeMint(lord.address) 59 | await token.safeMint(lord.address) 60 | 61 | expect(await token.totalSupply()) 62 | .to.eql(await token.balanceOf(lord.address)) 63 | .to.eql(BigNumber.from(3)) 64 | 65 | await expect( 66 | token.rentOut(renter.address, 1, expiresAt) 67 | ).to.be.revertedWith('ERC721: transfer from incorrect owner') 68 | 69 | await expect(token.connect(lord).rentOut(renter.address, 1, expiresAt)) 70 | .to.emit(token, 'Rented') 71 | .withArgs(1, lord.address, renter.address, expiresAt) 72 | 73 | expect( 74 | await Promise.all([ 75 | await token.totalSupply(), 76 | await token.balanceOf(lord.address), 77 | await token.balanceOf(renter.address), 78 | token.ownerOf(1) 79 | ]) 80 | ).to.eql([ 81 | BigNumber.from(3), 82 | constants.Two, 83 | constants.One, 84 | renter.address 85 | ]) 86 | 87 | const rental = await token.rental(1) 88 | 89 | expect([ 90 | rental.isActive, 91 | rental.lord, 92 | rental.renter, 93 | rental.expiresAt 94 | ]).to.eql([true, lord.address, renter.address, BigNumber.from(expiresAt)]) 95 | 96 | await expect( 97 | token.connect(renter).transferFrom(renter.address, guy.address, 1) 98 | ).to.be.revertedWith(`RentableNFT: this token is rented`) 99 | 100 | await expect(token.finishRenting(1)).to.be.revertedWith( 101 | `RentableNFT: this rental can't be finished` 102 | ) 103 | }) 104 | 105 | it('Early Finish', async () => { 106 | await expect(token.connect(renter).finishRenting(1)) 107 | .to.emit(token, 'FinishedRent') 108 | .withArgs(1, lord.address, renter.address, expiresAt) 109 | }) 110 | 111 | it('After Expiration', async () => { 112 | await network.provider.send('evm_setNextBlockTimestamp', [expiresAt]) 113 | 114 | await expect(token.connect(guy).finishRenting(1)) 115 | .to.emit(token, 'FinishedRent') 116 | .withArgs(1, lord.address, renter.address, expiresAt) 117 | }) 118 | 119 | afterEach(async () => { 120 | expect( 121 | await Promise.all([ 122 | await token.totalSupply(), 123 | await token.balanceOf(lord.address), 124 | await token.balanceOf(renter.address), 125 | token.ownerOf(1) 126 | ]) 127 | ).to.eql([ 128 | BigNumber.from(3), 129 | BigNumber.from(3), 130 | constants.Zero, 131 | lord.address 132 | ]) 133 | 134 | const rental = await token.rental(1) 135 | 136 | expect([ 137 | rental.isActive, 138 | rental.lord, 139 | rental.renter, 140 | rental.expiresAt 141 | ]).to.eql([ 142 | false, 143 | constants.AddressZero, 144 | constants.AddressZero, 145 | BigNumber.from(0) 146 | ]) 147 | }) 148 | }) 149 | }) 150 | --------------------------------------------------------------------------------