├── .env.template ├── .gitignore ├── .gitmodules ├── README.md ├── contracts ├── DecentralizedCasino.sol ├── DepositorCoin.sol ├── ERC20.sol ├── FixedPoint.sol ├── Oracle.sol └── Stablecoin.sol ├── foundry.toml ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── scripts ├── ERC20.s.sol └── deploy.ts ├── test ├── ERC20.t.sol ├── ERC20.ts ├── StableCoin.t.sol └── StableCoin.ts └── tsconfig.json /.env.template: -------------------------------------------------------------------------------- 1 | SEPOLIA_RPC_URL= 2 | PRIVATE_KEY= 3 | ETHERSCAN_API_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | .vscode 8 | 9 | # Hardhat files 10 | cache 11 | artifacts 12 | 13 | # Foundry files 14 | broadcast 15 | cache_forge 16 | out -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | branch = v1.5.1 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZeroToMastery StableCoin Hardhat Project 2 | 3 | This project is part of the [ZeroToMastery blockchain course](https://zerotomastery.io/courses/blockchain-developer-bootcamp/). It contains the code implemented throughout the course in a project combining Hardhat and Foundry. 4 | -------------------------------------------------------------------------------- /contracts/DecentralizedCasino.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | contract DecentralizedCasino { 5 | mapping(address => uint256) public gameWeiValues; 6 | mapping(address => uint256) public blockNumbersToBeUsed; 7 | 8 | address[] public lastThreeWinners; 9 | 10 | function playGame() public payable { 11 | uint256 blockNumberToBeUsed = blockNumbersToBeUsed[msg.sender]; 12 | 13 | if (blockNumberToBeUsed == 0) { 14 | // first run, determine block number to be used 15 | blockNumbersToBeUsed[msg.sender] = block.number + 128; 16 | gameWeiValues[msg.sender] = msg.value; 17 | return; 18 | } 19 | 20 | require(block.number > blockNumbersToBeUsed[msg.sender], "Too early"); 21 | require(block.number < blockNumbersToBeUsed[msg.sender], "Too late"); 22 | 23 | uint256 randomNumber = block.prevrandao; 24 | 25 | if (randomNumber % 2 == 0) { 26 | uint256 winningAmount = gameWeiValues[msg.sender] * 2; 27 | (bool success, ) = msg.sender.call{value: winningAmount}(""); 28 | require(success, "Transfer failed."); 29 | 30 | lastThreeWinners.push(msg.sender); 31 | 32 | if (lastThreeWinners.length > 3) { 33 | lastThreeWinners[0] = lastThreeWinners[1]; 34 | lastThreeWinners[1] = lastThreeWinners[2]; 35 | lastThreeWinners[2] = lastThreeWinners[3]; 36 | lastThreeWinners.pop(); 37 | } 38 | } 39 | 40 | blockNumbersToBeUsed[msg.sender] = 0; 41 | gameWeiValues[msg.sender] = 0; 42 | } 43 | 44 | receive() external payable { 45 | playGame(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/DepositorCoin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import {ERC20} from "./ERC20.sol"; 5 | 6 | contract DepositorCoin is ERC20 { 7 | address public owner; 8 | uint256 public unlockTime; 9 | 10 | modifier isUnlocked() { 11 | require(block.timestamp >= unlockTime, "DPC: Still locked"); 12 | _; 13 | } 14 | 15 | constructor( 16 | string memory _name, 17 | string memory _symbol, 18 | uint256 _lockTime, 19 | address _initialOwner, 20 | uint256 _initialSupply 21 | ) ERC20(_name, _symbol, 18) { 22 | owner = msg.sender; 23 | unlockTime = block.timestamp + _lockTime; 24 | 25 | _mint(_initialOwner, _initialSupply); 26 | } 27 | 28 | function mint(address to, uint256 value) external isUnlocked { 29 | require(msg.sender == owner, "DPC: Only owner can mint"); 30 | 31 | _mint(to, value); 32 | } 33 | 34 | function burn(address from, uint256 value) external isUnlocked { 35 | require(msg.sender == owner, "DPC: Only owner can burn"); 36 | 37 | _burn(from, value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import {console} from "hardhat/console.sol"; 5 | 6 | contract ERC20 { 7 | event Transfer(address indexed from, address indexed to, uint256 value); 8 | event Approval( 9 | address indexed owner, 10 | address indexed spender, 11 | uint256 value 12 | ); 13 | 14 | string public name; 15 | string public symbol; 16 | uint8 public immutable decimals; 17 | uint256 public totalSupply; 18 | 19 | mapping(address => uint256) public balanceOf; 20 | mapping(address => mapping(address => uint256)) public allowance; 21 | 22 | constructor(string memory _name, string memory _symbol, uint8 _decimals) { 23 | name = _name; 24 | symbol = _symbol; 25 | decimals = _decimals; 26 | } 27 | 28 | function transfer(address to, uint256 value) external returns (bool) { 29 | return _transfer(msg.sender, to, value); 30 | } 31 | 32 | function _mint(address to, uint256 value) internal { 33 | balanceOf[to] += value; 34 | totalSupply += value; 35 | 36 | emit Transfer(address(0), to, value); 37 | } 38 | 39 | function _burn(address from, uint256 value) internal { 40 | balanceOf[from] -= value; 41 | totalSupply -= value; 42 | 43 | emit Transfer(from, address(0), value); 44 | } 45 | 46 | function transferFrom( 47 | address from, 48 | address to, 49 | uint256 value 50 | ) external returns (bool) { 51 | require( 52 | allowance[from][msg.sender] >= value, 53 | "ERC20: Insufficient allowance" 54 | ); 55 | 56 | allowance[from][msg.sender] -= value; 57 | 58 | emit Approval(from, msg.sender, allowance[from][msg.sender]); 59 | 60 | return _transfer(from, to, value); 61 | } 62 | 63 | function _transfer( 64 | address from, 65 | address to, 66 | uint256 value 67 | ) private returns (bool) { 68 | require(balanceOf[from] >= value, "ERC20: Insufficient sender balance"); 69 | 70 | emit Transfer(from, to, value); 71 | 72 | balanceOf[from] -= value; 73 | balanceOf[to] += value; 74 | 75 | return true; 76 | } 77 | 78 | function approve( 79 | address spender, 80 | uint256 value 81 | ) external payable returns (bool) { 82 | allowance[msg.sender][spender] += value; 83 | 84 | emit Approval(msg.sender, spender, value); 85 | 86 | return true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /contracts/FixedPoint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.19; 3 | 4 | type FixedPoint is uint256; 5 | 6 | using {add as +} for FixedPoint global; 7 | using {sub as -} for FixedPoint global; 8 | using {mul as *} for FixedPoint global; 9 | using {div as /} for FixedPoint global; 10 | 11 | uint256 constant DECIMALS = 1e18; 12 | 13 | function add(FixedPoint a, FixedPoint b) pure returns (FixedPoint) { 14 | return FixedPoint.wrap(FixedPoint.unwrap(a) + FixedPoint.unwrap(b)); 15 | } 16 | 17 | function sub(FixedPoint a, FixedPoint b) pure returns (FixedPoint) { 18 | return FixedPoint.wrap(FixedPoint.unwrap(a) - FixedPoint.unwrap(b)); 19 | } 20 | 21 | function mul(FixedPoint a, FixedPoint b) pure returns (FixedPoint) { 22 | return FixedPoint.wrap(FixedPoint.unwrap(a) * FixedPoint.unwrap(b) / DECIMALS); 23 | } 24 | 25 | function div(FixedPoint a, FixedPoint b) pure returns (FixedPoint) { 26 | return FixedPoint.wrap(FixedPoint.unwrap(a) * DECIMALS / FixedPoint.unwrap(b)); 27 | } 28 | 29 | function fromFraction(uint256 numerator, uint256 denominator) pure returns (FixedPoint) { 30 | return FixedPoint.wrap(numerator * DECIMALS / denominator); 31 | } 32 | 33 | function mulFixedPoint(uint256 a, FixedPoint b) pure returns (uint256) { 34 | return a * FixedPoint.unwrap(b) / DECIMALS; 35 | } 36 | 37 | function divFixedPoint(uint256 a, FixedPoint b) pure returns (uint256) { 38 | return a * DECIMALS / FixedPoint.unwrap(b); 39 | } -------------------------------------------------------------------------------- /contracts/Oracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | contract Oracle { 5 | uint256 private price; 6 | address public owner; 7 | 8 | constructor() { 9 | owner = msg.sender; 10 | } 11 | 12 | function getPrice() external view returns (uint256) { 13 | return price; 14 | } 15 | 16 | function setPrice(uint256 newPrice) external { 17 | require(msg.sender == owner, "Oracle: only owner can set price"); 18 | price = newPrice; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/Stablecoin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import {ERC20} from "./ERC20.sol"; 5 | import {DepositorCoin} from "./DepositorCoin.sol"; 6 | import {Oracle} from "./Oracle.sol"; 7 | import {FixedPoint, fromFraction, mulFixedPoint, divFixedPoint} from "./FixedPoint.sol"; 8 | 9 | contract Stablecoin is ERC20 { 10 | DepositorCoin public depositorCoin; 11 | Oracle public oracle; 12 | uint256 public feeRatePercentage; 13 | uint256 public initialCollateralRatioPercentage; 14 | uint256 public depositorCoinLockTime; 15 | 16 | error InitialCollateralRatioError( 17 | string message, 18 | uint256 minimumDepositAmount 19 | ); 20 | 21 | constructor( 22 | string memory _name, 23 | string memory _symbol, 24 | Oracle _oracle, 25 | uint256 _feeRatePercentage, 26 | uint256 _initialCollateralRatioPercentage, 27 | uint256 _depositorCoinLockTime 28 | ) ERC20(_name, _symbol, 18) { 29 | oracle = _oracle; 30 | feeRatePercentage = _feeRatePercentage; 31 | initialCollateralRatioPercentage = _initialCollateralRatioPercentage; 32 | depositorCoinLockTime = _depositorCoinLockTime; 33 | } 34 | 35 | function mint() external payable { 36 | uint256 fee = _getFee(msg.value); 37 | uint256 mintStablecoinAmount = (msg.value - fee) * oracle.getPrice(); 38 | _mint(msg.sender, mintStablecoinAmount); 39 | } 40 | 41 | function burn(uint256 burnStablecoinAmount) external { 42 | _burn(msg.sender, burnStablecoinAmount); 43 | 44 | uint256 refundingEth = burnStablecoinAmount / oracle.getPrice(); 45 | uint256 fee = _getFee(refundingEth); 46 | 47 | (bool success, ) = msg.sender.call{value: (refundingEth - fee)}(""); 48 | require(success, "STC: Burn refund transaction failed"); 49 | } 50 | 51 | function _getFee(uint256 ethAmount) private view returns (uint256) { 52 | return (ethAmount * feeRatePercentage) / 100; 53 | } 54 | 55 | function depositCollateralBuffer() external payable { 56 | int256 deficitOrSurplusInUsd = _getDeficitOrSurplusInContractInUsd(); 57 | 58 | if (deficitOrSurplusInUsd <= 0) { 59 | uint256 deficitInUsd = uint256(deficitOrSurplusInUsd * -1); 60 | uint256 deficitInEth = deficitInUsd / oracle.getPrice(); 61 | 62 | uint256 addedSurplusEth = msg.value - deficitInEth; 63 | 64 | uint256 requiredInitialSurplusInUsd = (initialCollateralRatioPercentage * 65 | totalSupply) / 100; 66 | uint256 requiredInitialSurplusInEth = requiredInitialSurplusInUsd / 67 | oracle.getPrice(); 68 | 69 | if (addedSurplusEth < requiredInitialSurplusInEth) { 70 | uint256 minimumDeposit = deficitInEth + 71 | requiredInitialSurplusInEth; 72 | 73 | revert InitialCollateralRatioError( 74 | "STC: Initial collateral ratio not met, minimum is", 75 | minimumDeposit 76 | ); 77 | } 78 | 79 | uint256 initialDepositorSupply = addedSurplusEth * 80 | oracle.getPrice(); 81 | depositorCoin = new DepositorCoin( 82 | "Depositor Coin", 83 | "DPC", 84 | depositorCoinLockTime, 85 | msg.sender, 86 | initialDepositorSupply 87 | ); 88 | // new surplus: (msg.value - deficitInEth) * oracle.getPrice(); 89 | 90 | return; 91 | } 92 | 93 | uint256 surplusInUsd = uint256(deficitOrSurplusInUsd); 94 | 95 | // usdInDpcPrice = 250 / 500 = 0.5e18 96 | FixedPoint usdInDpcPrice = fromFraction( 97 | depositorCoin.totalSupply(), 98 | surplusInUsd 99 | ); 100 | 101 | // 0.5e18 * 1000 * 0.5e18 = 250e36 102 | uint256 mintDepositorCoinAmount = mulFixedPoint( 103 | msg.value * oracle.getPrice(), 104 | usdInDpcPrice 105 | ); 106 | depositorCoin.mint(msg.sender, mintDepositorCoinAmount); 107 | } 108 | 109 | function withdrawCollateralBuffer( 110 | uint256 burnDepositorCoinAmount 111 | ) external { 112 | int256 deficitOrSurplusInUsd = _getDeficitOrSurplusInContractInUsd(); 113 | require( 114 | deficitOrSurplusInUsd > 0, 115 | "STC: No depositor funds to withdraw" 116 | ); 117 | 118 | uint256 surplusInUsd = uint256(deficitOrSurplusInUsd); 119 | depositorCoin.burn(msg.sender, burnDepositorCoinAmount); 120 | 121 | // usdInDpcPrice = 250 / 500 = 0.5 122 | FixedPoint usdInDpcPrice = fromFraction( 123 | depositorCoin.totalSupply(), 124 | surplusInUsd 125 | ); 126 | 127 | // 125 / 0.5 = 250 128 | uint256 refundingUsd = divFixedPoint( 129 | burnDepositorCoinAmount, 130 | usdInDpcPrice 131 | ); 132 | 133 | // 250 / 1000 = 0.25 ETH 134 | uint256 refundingEth = refundingUsd / oracle.getPrice(); 135 | 136 | (bool success, ) = msg.sender.call{value: refundingEth}(""); 137 | require(success, "STC: Withdraw collateral buffer transaction failed"); 138 | } 139 | 140 | function _getDeficitOrSurplusInContractInUsd() 141 | private 142 | view 143 | returns (int256) 144 | { 145 | uint256 ethContractBalanceInUsd = (address(this).balance - msg.value) * 146 | oracle.getPrice(); 147 | 148 | uint256 totalStableCoinBalanceInUsd = totalSupply; 149 | 150 | int256 surplusOrDeficit = int256(ethContractBalanceInUsd) - 151 | int256(totalStableCoinBalanceInUsd); 152 | return surplusOrDeficit; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | cache_path = 'cache_forge' 3 | libs = [ 'lib', 'node_modules' ] 4 | out = 'out' 5 | src = 'contracts' 6 | test = 'test' 7 | 8 | [rpc_endpoints] 9 | sepolia = "${SEPOLIA_RPC_URL}" 10 | 11 | [etherscan] 12 | sepolia = { key = "${ETHERSCAN_API_KEY}" } 13 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | import { HardhatUserConfig } from "hardhat/config"; 4 | import "@nomicfoundation/hardhat-toolbox"; 5 | import "@nomicfoundation/hardhat-foundry"; 6 | 7 | const config: HardhatUserConfig = { 8 | solidity: { 9 | version: "0.8.19", 10 | settings: { 11 | outputSelection: { 12 | "*": { 13 | "*": ["storageLayout"], 14 | }, 15 | }, 16 | }, 17 | }, 18 | networks: { 19 | sepolia: { 20 | url: process.env.SEPOLIA_RPC_URL, 21 | accounts: [process.env.PRIVATE_KEY as string], 22 | }, 23 | }, 24 | etherscan: { 25 | apiKey: process.env.ETHERSCAN_API_KEY, 26 | }, 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my_erc20_hardhat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@defi-wonderland/smock": "^2.3.4", 14 | "@nomicfoundation/hardhat-foundry": "^1.0.0", 15 | "dotenv": "^16.0.3", 16 | "hardhat": "^2.13.0" 17 | }, 18 | "devDependencies": { 19 | "@nomicfoundation/hardhat-toolbox": "^2.0.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/ERC20.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | // --verify not yet working in Hardhat project 5 | 6 | import {Script} from "forge-std/Script.sol"; 7 | import {ERC20} from "../contracts/ERC20.sol"; 8 | 9 | contract ERC20Script is Script { 10 | function setUp() public {} 11 | 12 | function run() public { 13 | uint256 key = vm.envUint("PRIVATE_KEY"); 14 | vm.broadcast(key); 15 | 16 | new ERC20("Name", "SYM", 18); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | async function main() { 4 | const ERC20 = await ethers.getContractFactory("ERC20"); 5 | const erc20 = await ERC20.deploy("Name", "SYM", 18); 6 | console.log("ERC20 deployed to", erc20.address); 7 | } 8 | 9 | main().catch((error) => { 10 | console.error(error); 11 | process.exitCode = 1; 12 | }); 13 | -------------------------------------------------------------------------------- /test/ERC20.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import {Test, console2, StdStyle, StdCheats} from "forge-std/Test.sol"; 5 | import {ERC20} from "../contracts/ERC20.sol"; 6 | 7 | contract BaseSetup is ERC20, Test { 8 | address internal alice; 9 | address internal bob; 10 | 11 | constructor() ERC20("name", "SYM", 18) {} 12 | 13 | function setUp() public virtual { 14 | alice = makeAddr("alice"); 15 | bob = makeAddr("bob"); 16 | 17 | console2.log(StdStyle.blue("When Alice has 300 Tokens")); 18 | deal(address(this), alice, 300e18); 19 | } 20 | } 21 | 22 | contract ERC20TransferTest is BaseSetup { 23 | function setUp() public override { 24 | BaseSetup.setUp(); 25 | } 26 | 27 | function testTransfersTokenCorrectly() public { 28 | vm.prank(alice); 29 | bool success = this.transfer(bob, 100e18); 30 | assertTrue(success); 31 | 32 | assertEqDecimal(this.balanceOf(alice), 200e18, decimals); 33 | assertEqDecimal(this.balanceOf(bob), 100e18, decimals); 34 | } 35 | 36 | function testCannotTransferMoreThanBalance() public { 37 | vm.prank(alice); 38 | vm.expectRevert("ERC20: Insufficient sender balance"); 39 | this.transfer(bob, 400e18); 40 | } 41 | 42 | function testEmitsTransferEvent() public { 43 | vm.expectEmit(true, true, true, true); 44 | emit Transfer(alice, bob, 100e18); 45 | 46 | vm.prank(alice); 47 | this.transfer(bob, 100e18); 48 | } 49 | } 50 | 51 | contract ERC20TransferFromTest is BaseSetup {} 52 | -------------------------------------------------------------------------------- /test/ERC20.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import { expect } from "chai"; 3 | 4 | import { loadFixture, mine } from "@nomicfoundation/hardhat-network-helpers"; 5 | 6 | import { smock } from "@defi-wonderland/smock"; 7 | import { ERC20__factory } from "../typechain-types"; 8 | 9 | describe("ERC20", function () { 10 | async function deployAndMockERC20() { 11 | const [alice, bob] = await ethers.getSigners(); 12 | 13 | const ERC20 = await smock.mock("ERC20"); 14 | const erc20Token = await ERC20.deploy("Name", "SYM", 18); 15 | 16 | await erc20Token.setVariable("balanceOf", { 17 | [alice.address]: 300, 18 | }); 19 | await mine(); 20 | 21 | return { alice, bob, erc20Token }; 22 | } 23 | 24 | it("transfers tokens correctly", async function () { 25 | const { alice, bob, erc20Token } = await loadFixture(deployAndMockERC20); 26 | 27 | await expect( 28 | await erc20Token.transfer(bob.address, 100) 29 | ).to.changeTokenBalances(erc20Token, [alice, bob], [-100, 100]); 30 | 31 | await expect( 32 | await erc20Token.connect(bob).transfer(alice.address, 50) 33 | ).to.changeTokenBalances(erc20Token, [alice, bob], [50, -50]); 34 | }); 35 | 36 | it("should revert if sender has insufficient balance", async function () { 37 | const { bob, erc20Token } = await loadFixture(deployAndMockERC20); 38 | await expect(erc20Token.transfer(bob.address, 400)).to.be.revertedWith( 39 | "ERC20: Insufficient sender balance" 40 | ); 41 | }); 42 | 43 | it("should emit Transfer event on transfers", async function () { 44 | const { alice, bob, erc20Token } = await loadFixture(deployAndMockERC20); 45 | await expect(erc20Token.transfer(bob.address, 200)) 46 | .to.emit(erc20Token, "Transfer") 47 | .withArgs(alice.address, bob.address, 200); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/StableCoin.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | import {stdStorage, StdStorage, Test} from "forge-std/Test.sol"; 6 | 7 | import {Oracle} from "../contracts/Oracle.sol"; 8 | import {Stablecoin} from "../contracts/Stablecoin.sol"; 9 | 10 | contract BaseSetup is Stablecoin, Test { 11 | constructor() 12 | Stablecoin("Stablecoin", "STC", oracle = new Oracle(), 0, 10, 0) 13 | {} 14 | 15 | function setUp() public virtual { 16 | oracle.setPrice(4000); 17 | vm.deal(address(this), 0); 18 | } 19 | 20 | receive() external payable { 21 | console.log("Received ETH: %s", msg.value); 22 | } 23 | } 24 | 25 | contract StablecoinDeployedTests is BaseSetup { 26 | function testSetsFeeRatePercentage() public { 27 | assertEq(feeRatePercentage, 0); 28 | } 29 | 30 | function testAllowsMinting() public { 31 | uint256 ethAmount = 1e18; 32 | vm.deal(address(this), address(this).balance + ethAmount); 33 | this.mint{value: ethAmount}(); 34 | 35 | assertEq(totalSupply, ethAmount * oracle.getPrice()); 36 | } 37 | } 38 | 39 | contract WhenStablecoinMintedTokens is BaseSetup { 40 | uint256 internal mintAmount; 41 | 42 | function setUp() public virtual override { 43 | BaseSetup.setUp(); 44 | console.log("When user has minted tokens"); 45 | 46 | uint256 ethAmount = 1e18; 47 | mintAmount = ethAmount * oracle.getPrice(); 48 | 49 | vm.deal(address(this), address(this).balance + ethAmount); 50 | this.mint{value: ethAmount}(); 51 | } 52 | } 53 | 54 | contract MintedTokenTests is WhenStablecoinMintedTokens { 55 | function testShouldAllowBurning() public { 56 | uint256 remainingStablecoinAmount = 100; 57 | 58 | this.burn(mintAmount - remainingStablecoinAmount); 59 | assertEq(totalSupply, remainingStablecoinAmount); 60 | } 61 | 62 | function testCannotDepositBelowMin() public { 63 | uint256 stableCoinCollateralBuffer = 0.05e18; 64 | 65 | vm.deal( 66 | address(this), 67 | address(this).balance + stableCoinCollateralBuffer 68 | ); 69 | 70 | uint256 expectedMinimumDepositAmount = 0.1e18; 71 | 72 | vm.expectRevert( 73 | abi.encodeWithSelector( 74 | InitialCollateralRatioError.selector, 75 | "STC: Initial collateral ratio not met, minimum is", 76 | expectedMinimumDepositAmount 77 | ) 78 | ); 79 | this.depositCollateralBuffer{value: stableCoinCollateralBuffer}(); 80 | } 81 | 82 | function testShouldAllowDepositingInitialCollateralBuffer() public { 83 | uint256 stableCoinCollateralBuffer = 0.5e18; 84 | vm.deal( 85 | address(this), 86 | address(this).balance + stableCoinCollateralBuffer 87 | ); 88 | 89 | this.depositCollateralBuffer{value: stableCoinCollateralBuffer}(); 90 | 91 | uint256 newInitialSurplusInUsd = stableCoinCollateralBuffer * 92 | oracle.getPrice(); 93 | assertEq(this.depositorCoin().totalSupply(), newInitialSurplusInUsd); 94 | } 95 | } 96 | 97 | contract WhenDepositedCollateralBuffer is WhenStablecoinMintedTokens { 98 | uint256 internal stableCoinCollateralBuffer; 99 | 100 | function setUp() public override { 101 | WhenStablecoinMintedTokens.setUp(); 102 | console.log("When deposited collateral buffer"); 103 | 104 | stableCoinCollateralBuffer = 0.5e18; 105 | vm.deal( 106 | address(this), 107 | address(this).balance + stableCoinCollateralBuffer 108 | ); 109 | this.depositCollateralBuffer{value: stableCoinCollateralBuffer}(); 110 | } 111 | } 112 | 113 | contract DepositedCollateralBufferTests is WhenDepositedCollateralBuffer { 114 | function testShouldAllowWithdrawingCollateralBuffer() public { 115 | uint256 newDepositorTotalSupply = stableCoinCollateralBuffer * 116 | oracle.getPrice(); 117 | uint256 stableCoinCollateralBurnAmount = newDepositorTotalSupply / 5; 118 | 119 | this.withdrawCollateralBuffer(stableCoinCollateralBurnAmount); 120 | 121 | uint256 newDepositorSupply = newDepositorTotalSupply - 122 | stableCoinCollateralBurnAmount; 123 | assertEq(this.depositorCoin().totalSupply(), newDepositorSupply); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/StableCoin.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { DepositorCoin } from "../typechain-types"; 4 | import { Stablecoin } from "../typechain-types/Stablecoin"; 5 | 6 | describe("Stablecoin", function () { 7 | let ethUsdPrice: number, feeRatePercentage: number; 8 | let Stablecoin: Stablecoin; 9 | 10 | this.beforeEach(async () => { 11 | feeRatePercentage = 0; 12 | ethUsdPrice = 4000; 13 | 14 | const OracleFactory = await ethers.getContractFactory("Oracle"); 15 | const ethUsdOracle = await OracleFactory.deploy(); 16 | await ethUsdOracle.setPrice(ethUsdPrice); 17 | 18 | const StablecoinFactory = await ethers.getContractFactory("Stablecoin"); 19 | Stablecoin = await StablecoinFactory.deploy( 20 | "Stable Coin", 21 | "STC", 22 | ethUsdOracle.address, 23 | feeRatePercentage, 24 | 10, 25 | 0 26 | ); 27 | await Stablecoin.deployed(); 28 | }); 29 | 30 | it("Should set fee rate percentage", async function () { 31 | expect(await Stablecoin.feeRatePercentage()).to.equal(feeRatePercentage); 32 | }); 33 | 34 | it("Should allow minting", async function () { 35 | const ethAmount = 1; 36 | const expectedMintAmount = ethAmount * ethUsdPrice; 37 | 38 | await Stablecoin.mint({ 39 | value: ethers.utils.parseEther(ethAmount.toString()), 40 | }); 41 | expect(await Stablecoin.totalSupply()).to.equal( 42 | ethers.utils.parseEther(expectedMintAmount.toString()) 43 | ); 44 | }); 45 | 46 | describe("With minted tokens", function () { 47 | let mintAmount: number; 48 | 49 | this.beforeEach(async () => { 50 | const ethAmount = 1; 51 | mintAmount = ethAmount * ethUsdPrice; 52 | 53 | await Stablecoin.mint({ 54 | value: ethers.utils.parseEther(ethAmount.toString()), 55 | }); 56 | }); 57 | 58 | it("Should allow burning", async function () { 59 | const remainingStablecoinAmount = 100; 60 | await Stablecoin.burn( 61 | ethers.utils.parseEther( 62 | (mintAmount - remainingStablecoinAmount).toString() 63 | ) 64 | ); 65 | 66 | expect(await Stablecoin.totalSupply()).to.equal( 67 | ethers.utils.parseEther(remainingStablecoinAmount.toString()) 68 | ); 69 | }); 70 | 71 | it("Should prevent depositing collateral buffer below minimum", async function () { 72 | const stablecoinCollateralBuffer = 0.05; // less than minimum 73 | 74 | await expect( 75 | Stablecoin.depositCollateralBuffer({ 76 | value: ethers.utils.parseEther(stablecoinCollateralBuffer.toString()), 77 | }) 78 | ).to.be.revertedWithCustomError( 79 | Stablecoin, 80 | "InitialCollateralRatioError" 81 | ); 82 | }); 83 | 84 | it("Should allow depositing collateral buffer", async function () { 85 | const stablecoinCollateralBuffer = 0.5; 86 | await Stablecoin.depositCollateralBuffer({ 87 | value: ethers.utils.parseEther(stablecoinCollateralBuffer.toString()), 88 | }); 89 | 90 | const DepositorCoinFactory = await ethers.getContractFactory( 91 | "DepositorCoin" 92 | ); 93 | const DepositorCoin = await DepositorCoinFactory.attach( 94 | await Stablecoin.depositorCoin() 95 | ); 96 | 97 | const newInitialSurplusInUsd = stablecoinCollateralBuffer * ethUsdPrice; 98 | expect(await DepositorCoin.totalSupply()).to.equal( 99 | ethers.utils.parseEther(newInitialSurplusInUsd.toString()) 100 | ); 101 | }); 102 | 103 | describe("With deposited collateral buffer", function () { 104 | let stablecoinCollateralBuffer: number; 105 | let DepositorCoin: DepositorCoin; 106 | 107 | this.beforeEach(async () => { 108 | stablecoinCollateralBuffer = 0.5; 109 | await Stablecoin.depositCollateralBuffer({ 110 | value: ethers.utils.parseEther(stablecoinCollateralBuffer.toString()), 111 | }); 112 | 113 | const DepositorCoinFactory = await ethers.getContractFactory( 114 | "DepositorCoin" 115 | ); 116 | DepositorCoin = await DepositorCoinFactory.attach( 117 | await Stablecoin.depositorCoin() 118 | ); 119 | }); 120 | 121 | it("Should allow withdrawing collateral buffer", async function () { 122 | const newDepositorTotalSupply = 123 | stablecoinCollateralBuffer * ethUsdPrice; 124 | const stablecoinCollateralBurnAmount = newDepositorTotalSupply * 0.2; 125 | 126 | await Stablecoin.withdrawCollateralBuffer( 127 | ethers.utils.parseEther(stablecoinCollateralBurnAmount.toString()) 128 | ); 129 | 130 | expect(await DepositorCoin.totalSupply()).to.equal( 131 | ethers.utils.parseEther( 132 | ( 133 | newDepositorTotalSupply - stablecoinCollateralBurnAmount 134 | ).toString() 135 | ) 136 | ); 137 | }); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------