From 6dc7e758b8f7b5d7cdad7a73b0df7d8ced174f08 Mon Sep 17 00:00:00 2001 From: PatrickAlphac <54278053+PatrickAlphaC@users.noreply.github.com> Date: Thu, 16 Jun 2022 08:18:23 -0400 Subject: [PATCH 1/2] updated for centralized stablecoins and refactored some options testing --- .../stablecoins/AlgorithmicStablecoin.sol | 0 .../stablecoins/CentralizedStableCoin.sol | 163 +++++++ .../stablecoins/DecentralizedStablecoin.sol | 0 contracts/stablecoins/README.md | 41 ++ deploy/05-deploy-stablecoins.js | 29 ++ hardhat.config.js | 9 +- test/unit/centralizedStablecoin.test.js | 30 ++ test/unit/lending.test.js | 2 +- test/unit/options.test.js | 459 +++++++++++------- test/unit/staking.test.js | 4 +- 10 files changed, 547 insertions(+), 190 deletions(-) create mode 100644 contracts/stablecoins/AlgorithmicStablecoin.sol create mode 100644 contracts/stablecoins/CentralizedStableCoin.sol create mode 100644 contracts/stablecoins/DecentralizedStablecoin.sol create mode 100644 contracts/stablecoins/README.md create mode 100644 deploy/05-deploy-stablecoins.js create mode 100644 test/unit/centralizedStablecoin.test.js diff --git a/contracts/stablecoins/AlgorithmicStablecoin.sol b/contracts/stablecoins/AlgorithmicStablecoin.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/stablecoins/CentralizedStableCoin.sol b/contracts/stablecoins/CentralizedStableCoin.sol new file mode 100644 index 0000000..5df175e --- /dev/null +++ b/contracts/stablecoins/CentralizedStableCoin.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT + +// Based off: +// https://github.com/centrehq/centre-tokens/blob/master/contracts/v2/FiatTokenV2.sol +// https://github.com/centrehq/centre-tokens/blob/master/contracts/v1/FiatTokenV1.sol +// aka USDC +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +error CentralizedStablecoin__NotMinter(); +error CentralizedStablecoin__AddressBlacklisted(); +error CentralizedStablecoin__NotZeroAddress(); +error CentralizedStablecoin__AmountMustBeMoreThanZero(); +error CentralizedStablecoin__ExceededMinterAllowance(); +error CentralizedStablecoin__BurnAmountExceedsBalance(); + +contract CentralizedStableCoin is ERC20Burnable, Ownable { + mapping(address => bool) internal s_blacklisted; + mapping(address => bool) internal s_minters; + mapping(address => uint256) internal s_minterAllowed; + + // Events + event MinterConfigured(address indexed minter, uint256 minterAllowedAmount); + event MinterRemoved(address indexed oldMinter); + event Blacklisted(address indexed _account); + event UnBlacklisted(address indexed _account); + + // Modifiers + modifier onlyMinters() { + if (!s_minters[msg.sender]) { + revert CentralizedStablecoin__NotMinter(); + } + _; + } + + modifier notBlacklisted(address addressToCheck) { + if (s_blacklisted[addressToCheck]) { + revert CentralizedStablecoin__AddressBlacklisted(); + } + _; + } + + constructor(uint256 initialSupply) ERC20("CentralizedStablecoin", "CSC") { + _mint(msg.sender, initialSupply); + } + + function mint(address _to, uint256 _amount) + external + onlyMinters + notBlacklisted(msg.sender) + notBlacklisted(_to) + returns (bool) + { + if (_to == address(0)) { + revert CentralizedStablecoin__NotZeroAddress(); + } + if (_amount <= 0) { + revert CentralizedStablecoin__AmountMustBeMoreThanZero(); + } + + uint256 mintingAllowedAmount = s_minterAllowed[msg.sender]; + if (_amount <= mintingAllowedAmount) { + revert CentralizedStablecoin__ExceededMinterAllowance(); + } + _mint(msg.sender, mintingAllowedAmount); + return true; + } + + function burn(uint256 _amount) public override onlyMinters notBlacklisted(msg.sender) { + uint256 balance = balanceOf(msg.sender); + if (_amount <= 0) { + revert CentralizedStablecoin__AmountMustBeMoreThanZero(); + } + if (balance < _amount) { + revert CentralizedStablecoin__BurnAmountExceedsBalance(); + } + _burn(msg.sender, _amount); + } + + /***************************/ + /* Minter settings */ + /***************************/ + + function configureMinter(address minter, uint256 minterAllowedAmount) + external + onlyOwner + returns (bool) + { + s_minters[minter] = true; + s_minterAllowed[minter] = minterAllowedAmount; + emit MinterConfigured(minter, minterAllowedAmount); + return true; + } + + function removeMinter(address minter) external onlyOwner returns (bool) { + s_minters[minter] = false; + s_minterAllowed[minter] = 0; + emit MinterRemoved(minter); + return true; + } + + /***************************/ + /* Blacklisting Functions */ + /***************************/ + + function isBlacklisted(address _account) external view returns (bool) { + return s_blacklisted[_account]; + } + + function blacklist(address _account) external onlyOwner { + s_blacklisted[_account] = true; + emit Blacklisted(_account); + } + + function unBlacklist(address _account) external onlyOwner { + s_blacklisted[_account] = false; + emit UnBlacklisted(_account); + } + + /***************************/ + /* Blacklisting overrides */ + /***************************/ + + function approve(address spender, uint256 value) + public + override + notBlacklisted(msg.sender) + notBlacklisted(spender) + returns (bool) + { + super.approve(spender, value); + return true; + } + + function transferFrom( + address from, + address to, + uint256 value + ) + public + override + notBlacklisted(msg.sender) + notBlacklisted(from) + notBlacklisted(to) + returns (bool) + { + super.transferFrom(from, to, value); + return true; + } + + function transfer(address to, uint256 value) + public + override + notBlacklisted(msg.sender) + notBlacklisted(to) + returns (bool) + { + super.transfer(msg.sender, value); + return true; + } +} diff --git a/contracts/stablecoins/DecentralizedStablecoin.sol b/contracts/stablecoins/DecentralizedStablecoin.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/stablecoins/README.md b/contracts/stablecoins/README.md new file mode 100644 index 0000000..17925fd --- /dev/null +++ b/contracts/stablecoins/README.md @@ -0,0 +1,41 @@ +Maker has like a billion names for a billion things. + +The skinny of it is: +You deposit ETH -> it mints you DAI + +List of contract addresses for DAI: https://chainlog.makerdao.com/api/mainnet/active.json +Or with a specific version: https://changelog.makerdao.com/releases/mainnet/1.12.0/contracts.json +red-black coins: https://users.encs.concordia.ca/~clark/papers/2021_defi.pdf + + +Maker Oracles: [Per here](https://github.com/makerdao/developerguides/blob/master/oracles/oracle-integration-guide.md#oracle-module): The latest contract addresses for each collateral oracle contract can be found in the changelog. Each collateral asset will have a contract address named as PIP_collateralName. For example, for the ETH collateral type, you’ll find the oracle contract as this: PIP_ETH + +ETH PIP Contract Address: https://etherscan.io/address/0x81FE72B5A8d1A857d176C3E7d5Bd2679A9B85763#code + +Functions: +- `peek`: returns the price +- `peep`: returns the next price +- `read`: returns the price, reverts if price is bad + + +They read from a DSValue contract like this: https://github.com/dapphub/ds-value/blob/master/src/value.sol <- You can think of this as the "real" price feed contract, and OSM does the logic behind when they get pulled in. + + +This is the oracle 👇 +This is an EOA (not a contract) that calls `poke` methods: https://etherscan.io/address/0xb3f5130e287e6611323ad263e37ce763d4f129e8. But it could be really anyone since they already have the next price setup. + +Well.. it calls the megapoker which calls the OSM contract that updates the price. + +This contract keeps track of all the "pokers" https://etherscan.io/address/0xea347db6ef446e03745c441c17018ef3d641bc8f#code + +## Attack Vectors + +Anyone can call the "poke" function and move to the next price. This seems bad. But it showed up! I updated the price! It looks like it uses uniswap as the oracle, and then a set of keepers to call `poke` all the time! + +I called poke: +https://etherscan.io/tx/0x1554dd8ba35d29ad0d4d6cff4c4378bcc91f20ff9b9b648b4361a310dca4ccb7 + +1. Syncs all the oracle prices on the uniswap oracle +2. Do a transfer on curve? (to get prices?) +3. Then lido? +4. Then it updates all the values in the maker stuff? diff --git a/deploy/05-deploy-stablecoins.js b/deploy/05-deploy-stablecoins.js new file mode 100644 index 0000000..08fd0fa --- /dev/null +++ b/deploy/05-deploy-stablecoins.js @@ -0,0 +1,29 @@ +const { network, ethers } = require("hardhat") +const { developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS } = require("../helper-hardhat-config") +const { verify } = require("../helper-functions") + +module.exports = async ({ getNamedAccounts, deployments }) => { + const { deploy, log } = deployments + const { deployer } = await getNamedAccounts() + const waitBlockConfirmations = developmentChains.includes(network.name) + ? 1 + : VERIFICATION_BLOCK_CONFIRMATIONS + log("----------------------------------------------------") + const initialSupply = ethers.utils.parseUnits("10", "ether") + const args = [initialSupply] + const centralizedStablecoinDeployment = await deploy("CentralizedStableCoin", { + from: deployer, + args: args, + log: true, + waitConfirmations: waitBlockConfirmations, + }) + + // Verify the deployment + if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) { + log("Verifying...") + await verify(centralizedStablecoinDeployment.address, args) + } + log("----------------------------------------------------") +} + +module.exports.tags = ["all", "centralizedstablecoin"] diff --git a/hardhat.config.js b/hardhat.config.js index b3d9477..04cd313 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -41,12 +41,15 @@ module.exports = { rinkeby: { url: RINKEBY_RPC_URL, accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [], - // accounts: { - // mnemonic: MNEMONIC, - // }, saveDeployments: true, chainId: 4, }, + mainnet: { + url: MAINNET_RPC_URL, + accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [], + saveDeployments: true, + chainId: 1, + }, }, etherscan: { // yarn hardhat verify --network diff --git a/test/unit/centralizedStablecoin.test.js b/test/unit/centralizedStablecoin.test.js new file mode 100644 index 0000000..bb71296 --- /dev/null +++ b/test/unit/centralizedStablecoin.test.js @@ -0,0 +1,30 @@ +const { assert, expect } = require("chai") +const { network, deployments, ethers } = require("hardhat") +const { developmentChains } = require("../../helper-hardhat-config") + +!developmentChains.includes(network.name) + ? describe.skip + : describe("Centralized Stablecoin tests", function () { + let centralizedStablecoin, deployer, accounts + beforeEach(async () => { + accounts = await ethers.getSigners() + deployer = accounts[0] + await deployments.fixture(["centralizedstablecoin"]) + centralizedStablecoin = await ethers.getContract("CentralizedStableCoin") + }) + + it("Can blacklist", async function () { + const transferAmount = ethers.utils.parseUnits("1", "ether") + const blackListedAccount = accounts[1] + const blacklistTx = await centralizedStablecoin.blacklist(blackListedAccount.address) + await blacklistTx.wait(1) + await expect( + centralizedStablecoin.transfer(blackListedAccount.address, transferAmount) + ).to.be.revertedWith("CentralizedStablecoin__AddressBlacklisted()") + }) + + // incomplete + // it("Can mint", async function () {}) + // it("Can burn", async function () {}) + // it("only owner can assign minters and blacklist", async function () {}) + }) diff --git a/test/unit/lending.test.js b/test/unit/lending.test.js index 0cea03a..660775a 100644 --- a/test/unit/lending.test.js +++ b/test/unit/lending.test.js @@ -8,7 +8,7 @@ const BTC_UPDATED_PRICE = ethers.utils.parseEther("1.9") !developmentChains.includes(network.name) ? describe.skip - : describe("Lending Unit Tests", async function () { + : describe("Lending Unit Tests", function () { let lending, dai, wbtc, depositAmount, randomToken, player, threshold, wbtcEthPriceFeed beforeEach(async () => { const accounts = await ethers.getSigners() diff --git a/test/unit/options.test.js b/test/unit/options.test.js index 6107692..f3eda64 100644 --- a/test/unit/options.test.js +++ b/test/unit/options.test.js @@ -1,202 +1,293 @@ const { assert, expect } = require("chai") -const { network, deployments, ethers} = require("hardhat") +const { network, deployments, ethers } = require("hardhat") const { developmentChains } = require("../../helper-hardhat-config") const { moveBlocks } = require("../../utils/move-blocks") - !developmentChains.includes(network.name) ? describe.skip - : describe.only("Options unit tests", async () => { - let owner, writer, buyer - beforeEach(async () => { + : describe("Options unit tests", () => { + let owner, writer, buyer + beforeEach(async () => { const [account] = await ethers.getSigners() owner = account[0] writer = account[1] buyer = account[2] await deployments.fixture(["mocks", "options"]) - + daiEthPriceFeed = await ethers.getContract("DAIETHPriceFeed") dai = await ethers.getContract("DAI") options = await ethers.getContract("Options") depositAmt = ethers.utils.parseEther("1") + }) + describe("Get DAI/ETH price data from oracle", () => { + it("Should not be null", async () => { + const daiPerOneEth = ethers.utils.parseEther("1000") + const priceEthPerOneDai = await options.getPriceFeed(daiPerOneEth) + expect(priceEthPerOneDai).not.be.null + }) + it("Should return DAI/ETH price", async () => { + const daiPerOneEth = ethers.utils.parseEther("1000") + const price = await options.getPriceFeed(daiPerOneEth) + assert.equal(price.toString(), ethers.utils.parseEther("1").toString()) + console.log(`price: ${price / 1e18}`) + }) + }) + + describe("Init contract", () => { + it("Should deploy w/o incident", async () => { + address = options.address + assert.notEqual(address, "") + assert.notEqual(address, 0x0) + assert.notEqual(address, null) + assert.notEqual(address, undefined) + }) + }) + describe("Option parameters", () => { + const amount = 1 + const premiumDue = 1 + const daysToExpiry = 1 + + describe("Writing a call", () => { + const strike = 1000 + const daiAmount = 1000 + + it("Should use correct parameters", async () => { + const writeCall = await options.writeCallOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + expect(amount).to.equal(1) + expect(strike).to.equal(1000) + expect(premiumDue).to.equal(1) + expect(daysToExpiry).to.equal(1) + expect(daiAmount).to.equal(1000) + }) + it("Should emit call option event", async () => { + const writeCall = await options.writeCallOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + await expect(writeCall).to.emit(options, "CallOptionOpen") + }) + }) + describe("Buy call options", () => { + const strike = 1000 + const daiAmount = 1000 + + it("Should emit buy call option event", async () => { + await options.writeCallOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + + await dai.approve(options.address, depositAmt) + const buyCall = await options.buyCallOption(0, { from: buyer }) + + await expect(buyCall).to.emit(options, "CallOptionBought") + }) + it("Should be a call and reject a put", async () => { + await options.writeCallOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + await expect(options.buyPutOption(0, { from: buyer })).to.be.reverted + }) + }) + describe("Write a put option contract", () => { + const strike = 1000 + const daiAmount = 1000 + + it("Should use correct parameters", async () => { + const writePut = await options.writePutOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + expect(amount).to.equal(1) + expect(strike).to.equal(1000) + expect(premiumDue).to.equal(1) + expect(daysToExpiry).to.equal(1) + }) + it("Should emit open put option event", async () => { + const writePut = await options.writePutOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + await expect(writePut).to.emit(options, "PutOptionOpen") + }) + }) + describe("Buy put option", () => { + const strike = 1000 + const daiAmount = 1000 + + it("Should emit buy put option event", async () => { + await options.writePutOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + + await dai.approve(options.address, depositAmt) + const buyPut = await options.buyPutOption(0, { from: buyer }) + + await expect(buyPut).to.emit(options, "PutOptionBought") + }) + it("Should be a put and reject a call", async () => { + await options.writePutOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + await expect(options.buyCallOption(0, { from: buyer })).to.be.reverted + }) + }) + describe("Exercise call options", () => { + const strike = 1000 + const daiAmount = 1000 + + it("Should fail to emit event call option exercised because spot not greater than strike...", async () => { + await options.writeCallOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + + await dai.approve(options.address, depositAmt) + await options.buyCallOption(0, { from: buyer }) + await moveBlocks(1) + + await expect(options.exerciseCallOption(0, 1000, { from: buyer })).to.be + .reverted + }) + it("Should fail if strike > ETH spot price...", async () => { + await options.writeCallOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + + await dai.approve(options.address, depositAmt) + + await options.buyCallOption(0, { from: buyer }) + await moveBlocks(1) + + await expect(options.exerciseCallOption(0, 1, { from: buyer })).to.be.reverted + }) + }) + describe("Exercise put options", () => { + it("Should fail to emit event put option exercised because spot is not less than strike...", async () => { + const strike = 1000 + const daiAmount = 1000 + + await options.writePutOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + + await dai.approve(options.address, depositAmt) + + await options.buyPutOption(0, { from: buyer }) + await moveBlocks(1) + + await expect(options.exercisePutOption(0, 1000, { from: buyer })).to.be + .reverted + }) + }) + describe("Option expires worthless", () => { + it("Should fail to emit event b/c call option is not worthless if spot not less than strike...", async () => { + const strike = 1000 + const daiAmount = 1000 + + await options.writeCallOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + + await dai.approve(options.address, depositAmt) + + await options.buyCallOption(0, { from: buyer }) + await moveBlocks(1) + + await expect(options.optionExpiresWorthless(0, 1000, { from: buyer })).to.be + .reverted + + // const expiredCall = await options.optionExpiresWorthless(0, 1000, {from: buyer}) + // await expect(expiredCall).to.emit(options, 'OptionExpiresWorthless') + }) + it("Should fail to emit event for put option b/c it is not worthless at expiration...", async () => { + const strike = 1000 + const daiAmount = 1000 + + await options.writePutOption( + amount, + strike, + premiumDue, + daysToExpiry, + daiAmount, + { from: writer, value: 1 } + ) + + await dai.approve(options.address, depositAmt) + + await options.buyPutOption(0, { from: buyer }) + await moveBlocks(1) + + await expect(options.optionExpiresWorthless(0, 1000, { from: buyer })).to.be + .reverted - }); - describe("Get DAI/ETH price data from oracle", async () => { - it("Should not be null", async () => { - const daiPerOneEth = ethers.utils.parseEther("1000") - const priceEthPerOneDai = await options.getPriceFeed(daiPerOneEth) - expect(priceEthPerOneDai).not.be.null - }) - it("Should return DAI/ETH price", async () => { - const daiPerOneEth = ethers.utils.parseEther("1000") - const price = await options.getPriceFeed(daiPerOneEth) - assert.equal(price.toString(), ethers.utils.parseEther("1").toString()) - console.log(`price: ${price / 1e18}`) - }) - }) - - describe("Init contract", async() => { - it("Should deploy w/o incident", async() => { - address = options.address - assert.notEqual(address, '') - assert.notEqual(address, 0x0) - assert.notEqual(address, null) - assert.notEqual(address, undefined) - }) - }) - describe("Option parameters", async() => { - const amount = 1 - const premiumDue = 1 - const daysToExpiry = 1 - - describe("Writing a call", async() => { - const strike = 1000 - const daiAmount = 1000 - const writeCall = await options.writeCallOption(amount, strike, premiumDue, daysToExpiry, daiAmount, {from: writer, value:1}) - - it("Should use correct parameters", async() => { - expect(amount).to.equal(1) - expect(strike).to.equal(1000) - expect(premiumDue).to.equal(1) - expect(daysToExpiry).to.equal(1) - expect(daiAmount).to.equal(1000) - }) - it("Should emit call option event", async() => { - await expect(writeCall).to.emit(options, 'CallOptionOpen') - }) - }) - describe("Buy call options", async() => { - const strike = 1000 - const daiAmount = 1000 - - it("Should emit buy call option event", async() => { - await options.writeCallOption(amount, strike, premiumDue, daysToExpiry, daiAmount, {from: writer, value:1}) - - await dai.approve(options.address, depositAmt) - const buyCall = await options.buyCallOption(0, {from: buyer}) - - await expect(buyCall).to.emit(options, 'CallOptionBought') - }) - it("Should be a call and reject a put", async() => { - await options.writeCallOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1}) - await expect(options.buyPutOption(0, {from: buyer})).to.be.reverted - }) - }) - describe("Write a put option contract", async() => { - const strike = 1000 - const daiAmount = 1000 - - const writePut = await options.writePutOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value: 1}) - it("Should use correct parameters", async() => { - expect(amount).to.equal(1); - expect(strike).to.equal(1000); - expect(premiumDue).to.equal(1); - expect(daysToExpiry).to.equal(1); - }) - it("Should emit open put option event", async() => { - await expect(writePut).to.emit(options, 'PutOptionOpen') - }) - }) - describe("Buy put option", async() => { - const strike = 1000 - const daiAmount = 1000 - - it("Should emit buy put option event", async() => { - await options.writePutOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1}) - - await dai.approve(options.address, depositAmt) - const buyPut = await options.buyPutOption(0, {from: buyer}) - - await expect(buyPut).to.emit(options, 'PutOptionBought') - }) - it("Should be a put and reject a call", async() => { - await options.writePutOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1}) - await expect(options.buyCallOption(0, {from: buyer})).to.be.reverted - }) - }) - describe("Exercise call options", async() => { - const strike = 1000 - const daiAmount = 1000 - - - it("Should fail to emit event call option exercised because spot not greater than strike...", async() => { - await options.writeCallOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1}) - - await dai.approve(options.address, depositAmt) - await options.buyCallOption(0, {from: buyer}) - await moveBlocks(1) - - await expect(options.exerciseCallOption(0, 1000,{from: buyer})).to.be.reverted - }) - it("Should fail if strike > ETH spot price...", async() => { - await options.writeCallOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1 }) - - await dai.approve(options.address, depositAmt) - - await options.buyCallOption(0, {from: buyer}) - await moveBlocks(1) - - await expect(options.exerciseCallOption(0, 1,{from: buyer})).to.be.reverted - }) - }) - describe("Exercise put options", async() => { - - it("Should fail to emit event put option exercised because spot is not less than strike...", async() => { - const strike = 1000 - const daiAmount = 1000 - - await options.writePutOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1}) - - await dai.approve(options.address, depositAmt) - - await options.buyPutOption(0, {from: buyer}) - await moveBlocks(1) - - await expect(options.exercisePutOption(0, 1000, {from: buyer})).to.be.reverted - - }) - }) - describe("Option expires worthless", async() => { - - it("Should fail to emit event b/c call option is not worthless if spot not less than strike...", async() => { - const strike = 1000 - const daiAmount = 1000 - - await options.writeCallOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1}) - - await dai.approve(options.address, depositAmt) - - await options.buyCallOption(0, {from: buyer}) - await moveBlocks(1) - - await expect(options.optionExpiresWorthless(0, 1000, {from: buyer})).to.be.reverted - - // const expiredCall = await options.optionExpiresWorthless(0, 1000, {from: buyer}) - // await expect(expiredCall).to.emit(options, 'OptionExpiresWorthless') - }) - it("Should fail to emit event for put option b/c it is not worthless at expiration...", async() => { - const strike = 1000 - const daiAmount = 1000 - - await options.writePutOption(amount, strike, premiumDue, daysToExpiry,daiAmount, {from: writer, value:1}) - - await dai.approve(options.address, depositAmt) - - await options.buyPutOption(0, {from: buyer}) - await moveBlocks(1) - - await expect(options.optionExpiresWorthless(0, 1000, {from: buyer})).to.be.reverted - - // const expiredPut = await options.optionExpiresWorthless(0, 1000, {from: buyer}) - // await expect(expiredPut).to.emit(options, 'OptionExpiresWorthless') - }) - }) - describe("Writer gets funds back", async() => { - it("Should fail because option is not canceled...", async() => { - await expect(options.retrieveExpiredFunds(0, {from:writer})).to.be.reverted - }) - }) - }) -}) \ No newline at end of file + // const expiredPut = await options.optionExpiresWorthless(0, 1000, {from: buyer}) + // await expect(expiredPut).to.emit(options, 'OptionExpiresWorthless') + }) + }) + describe("Writer gets funds back", () => { + it("Should fail because option is not canceled...", async () => { + await expect(options.retrieveExpiredFunds(0, { from: writer })).to.be.reverted + }) + }) + }) + }) diff --git a/test/unit/staking.test.js b/test/unit/staking.test.js index 74a70c3..933a828 100644 --- a/test/unit/staking.test.js +++ b/test/unit/staking.test.js @@ -9,8 +9,8 @@ const SECONDS_IN_A_YEAR = 31449600 !developmentChains.includes(network.name) ? describe.skip - : describe("Staking Unit Tests", async function () { - let staking, rewardToken, deployer, stakeAmount + : describe("Staking Unit Tests", function () { + let staking, rewardToken, deployer, dai, stakeAmount beforeEach(async () => { const accounts = await ethers.getSigners() deployer = accounts[0] From cffc479d239bbb0780aab5ee50f8c5e31d954ac6 Mon Sep 17 00:00:00 2001 From: PatrickAlphac <54278053+PatrickAlphaC@users.noreply.github.com> Date: Thu, 14 Jul 2022 14:29:47 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=AA=99=20updated=20for=20a=20USDC-lik?= =?UTF-8?q?e=20and=20DAI-like=20stablecoin=20minimal=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + README.md | 7 +- .../stablecoins/AlgorithmicStablecoin.sol | 0 .../stablecoins/CentralizedStableCoin.sol | 40 ++- .../stablecoins/DecentralizedStablecoin.sol | 0 contracts/stablecoins/README.md | 41 --- .../AlgorithmicStableCoin.sol | 23 ++ .../stablecoins/endogenousIndexCoin/Fpi.sol | 3 + .../exogenousAnchoredCoin/DSCEngine.sol | 293 ++++++++++++++++++ .../DecentralizedStableCoin.sol | 49 +++ .../exogenousIndexCoin/ReflexorIndex.sol | 4 + deploy/00-deploy-mocks.js | 32 ++ ...s => 05-deploy-centralized-stablecoins.js} | 0 deploy/06-deploy-decentralized-stablecoin.js | 77 +++++ helper-hardhat-config.js | 4 + test/unit/centralizedStablecoin.test.js | 30 -- .../stablecoins/centralizedStableCoin.test.js | 71 +++++ .../decentralizedStableCoin.test.js | 114 +++++++ 18 files changed, 703 insertions(+), 87 deletions(-) delete mode 100644 contracts/stablecoins/AlgorithmicStablecoin.sol delete mode 100644 contracts/stablecoins/DecentralizedStablecoin.sol delete mode 100644 contracts/stablecoins/README.md create mode 100644 contracts/stablecoins/endogenousAnchoredCoin/AlgorithmicStableCoin.sol create mode 100644 contracts/stablecoins/endogenousIndexCoin/Fpi.sol create mode 100644 contracts/stablecoins/exogenousAnchoredCoin/DSCEngine.sol create mode 100644 contracts/stablecoins/exogenousAnchoredCoin/DecentralizedStableCoin.sol create mode 100644 contracts/stablecoins/exogenousIndexCoin/ReflexorIndex.sol rename deploy/{05-deploy-stablecoins.js => 05-deploy-centralized-stablecoins.js} (100%) create mode 100644 deploy/06-deploy-decentralized-stablecoin.js delete mode 100644 test/unit/centralizedStablecoin.test.js create mode 100644 test/unit/stablecoins/centralizedStableCoin.test.js create mode 100644 test/unit/stablecoins/decentralizedStableCoin.test.js diff --git a/.gitignore b/.gitignore index 9d4f81b..833303e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ deployments .DS_Store .video.md .article.md +contracts/stablecoins/README.md + diff --git a/README.md b/README.md index f4e8505..45b88f9 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,21 @@ This repo is dedicated to making minimal repos of existing defi primatives. - `Staking.sol`: Based off [Synthetix](https://synthetix.io/) - `RewardToken.sol`: Based off [Synthetix](https://synthetix.io/) - `Exchange.sol` , `Factory.sol` , `Token.sol` : Based off [Uniswap v1](https://docs.uniswap.org/protocol/V1/introduction). The used pricing formula is documented [here](./docs/uniswap-v1/) +- `DecentralizedStableCoin`: Based off DAI/RAI +- `CentralizedStableCoin`: Based off USDC ### Uncompleted: - `Options.sol`: Based off nothing ### Not a minimal contract: -- `Swap.sol`: Based off [Uniswap](https://uniswap.org/) +- `Swap.sol`: Based off [Uniswap](https://uniswap.org/) - shows how a smart contract can integrate with a Uniswap-like dex. # Table Of Contents - [Defi Minimal](#defi-minimal) - - [Completed minimal contracts:](#completed-minimal-contracts) + - [Completed (but unreviewed) minimal contracts:](#completed-but-unreviewed-minimal-contracts) - [Uncompleted:](#uncompleted) - [Not a minimal contract:](#not-a-minimal-contract) +- [Table Of Contents](#table-of-contents) - [Getting Started](#getting-started) - [Requirements](#requirements) - [Quickstart](#quickstart) diff --git a/contracts/stablecoins/AlgorithmicStablecoin.sol b/contracts/stablecoins/AlgorithmicStablecoin.sol deleted file mode 100644 index e69de29..0000000 diff --git a/contracts/stablecoins/CentralizedStableCoin.sol b/contracts/stablecoins/CentralizedStableCoin.sol index 5df175e..174c5b0 100644 --- a/contracts/stablecoins/CentralizedStableCoin.sol +++ b/contracts/stablecoins/CentralizedStableCoin.sol @@ -4,17 +4,28 @@ // https://github.com/centrehq/centre-tokens/blob/master/contracts/v2/FiatTokenV2.sol // https://github.com/centrehq/centre-tokens/blob/master/contracts/v1/FiatTokenV1.sol // aka USDC + +// This is considered an exogenous, centralized, anchored (pegged), fiat collateralized, low volitility coin + +// Collateral: Exogenous +// Minting: Centralized +// Value: Anchored (Pegged to USD) +// Collateral Type: Fiat + +// Also sometimes just refered to as "Fiat Collateralized Stablecoin" +// But maybe a better name would be "FiatCoin" + pragma solidity ^0.8.7; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -error CentralizedStablecoin__NotMinter(); -error CentralizedStablecoin__AddressBlacklisted(); -error CentralizedStablecoin__NotZeroAddress(); -error CentralizedStablecoin__AmountMustBeMoreThanZero(); -error CentralizedStablecoin__ExceededMinterAllowance(); -error CentralizedStablecoin__BurnAmountExceedsBalance(); +error CentralizedStableCoin__NotMinter(); +error CentralizedStableCoin__AddressBlacklisted(); +error CentralizedStableCoin__NotZeroAddress(); +error CentralizedStableCoin__AmountMustBeMoreThanZero(); +error CentralizedStableCoin__ExceededMinterAllowance(); +error CentralizedStableCoin__BurnAmountExceedsBalance(); contract CentralizedStableCoin is ERC20Burnable, Ownable { mapping(address => bool) internal s_blacklisted; @@ -30,14 +41,14 @@ contract CentralizedStableCoin is ERC20Burnable, Ownable { // Modifiers modifier onlyMinters() { if (!s_minters[msg.sender]) { - revert CentralizedStablecoin__NotMinter(); + revert CentralizedStableCoin__NotMinter(); } _; } modifier notBlacklisted(address addressToCheck) { if (s_blacklisted[addressToCheck]) { - revert CentralizedStablecoin__AddressBlacklisted(); + revert CentralizedStableCoin__AddressBlacklisted(); } _; } @@ -54,16 +65,17 @@ contract CentralizedStableCoin is ERC20Burnable, Ownable { returns (bool) { if (_to == address(0)) { - revert CentralizedStablecoin__NotZeroAddress(); + revert CentralizedStableCoin__NotZeroAddress(); } if (_amount <= 0) { - revert CentralizedStablecoin__AmountMustBeMoreThanZero(); + revert CentralizedStableCoin__AmountMustBeMoreThanZero(); } uint256 mintingAllowedAmount = s_minterAllowed[msg.sender]; - if (_amount <= mintingAllowedAmount) { - revert CentralizedStablecoin__ExceededMinterAllowance(); + if (_amount > mintingAllowedAmount) { + revert CentralizedStableCoin__ExceededMinterAllowance(); } + s_minterAllowed[msg.sender] = mintingAllowedAmount - _amount; _mint(msg.sender, mintingAllowedAmount); return true; } @@ -71,10 +83,10 @@ contract CentralizedStableCoin is ERC20Burnable, Ownable { function burn(uint256 _amount) public override onlyMinters notBlacklisted(msg.sender) { uint256 balance = balanceOf(msg.sender); if (_amount <= 0) { - revert CentralizedStablecoin__AmountMustBeMoreThanZero(); + revert CentralizedStableCoin__AmountMustBeMoreThanZero(); } if (balance < _amount) { - revert CentralizedStablecoin__BurnAmountExceedsBalance(); + revert CentralizedStableCoin__BurnAmountExceedsBalance(); } _burn(msg.sender, _amount); } diff --git a/contracts/stablecoins/DecentralizedStablecoin.sol b/contracts/stablecoins/DecentralizedStablecoin.sol deleted file mode 100644 index e69de29..0000000 diff --git a/contracts/stablecoins/README.md b/contracts/stablecoins/README.md deleted file mode 100644 index 17925fd..0000000 --- a/contracts/stablecoins/README.md +++ /dev/null @@ -1,41 +0,0 @@ -Maker has like a billion names for a billion things. - -The skinny of it is: -You deposit ETH -> it mints you DAI - -List of contract addresses for DAI: https://chainlog.makerdao.com/api/mainnet/active.json -Or with a specific version: https://changelog.makerdao.com/releases/mainnet/1.12.0/contracts.json -red-black coins: https://users.encs.concordia.ca/~clark/papers/2021_defi.pdf - - -Maker Oracles: [Per here](https://github.com/makerdao/developerguides/blob/master/oracles/oracle-integration-guide.md#oracle-module): The latest contract addresses for each collateral oracle contract can be found in the changelog. Each collateral asset will have a contract address named as PIP_collateralName. For example, for the ETH collateral type, you’ll find the oracle contract as this: PIP_ETH - -ETH PIP Contract Address: https://etherscan.io/address/0x81FE72B5A8d1A857d176C3E7d5Bd2679A9B85763#code - -Functions: -- `peek`: returns the price -- `peep`: returns the next price -- `read`: returns the price, reverts if price is bad - - -They read from a DSValue contract like this: https://github.com/dapphub/ds-value/blob/master/src/value.sol <- You can think of this as the "real" price feed contract, and OSM does the logic behind when they get pulled in. - - -This is the oracle 👇 -This is an EOA (not a contract) that calls `poke` methods: https://etherscan.io/address/0xb3f5130e287e6611323ad263e37ce763d4f129e8. But it could be really anyone since they already have the next price setup. - -Well.. it calls the megapoker which calls the OSM contract that updates the price. - -This contract keeps track of all the "pokers" https://etherscan.io/address/0xea347db6ef446e03745c441c17018ef3d641bc8f#code - -## Attack Vectors - -Anyone can call the "poke" function and move to the next price. This seems bad. But it showed up! I updated the price! It looks like it uses uniswap as the oracle, and then a set of keepers to call `poke` all the time! - -I called poke: -https://etherscan.io/tx/0x1554dd8ba35d29ad0d4d6cff4c4378bcc91f20ff9b9b648b4361a310dca4ccb7 - -1. Syncs all the oracle prices on the uniswap oracle -2. Do a transfer on curve? (to get prices?) -3. Then lido? -4. Then it updates all the values in the maker stuff? diff --git a/contracts/stablecoins/endogenousAnchoredCoin/AlgorithmicStableCoin.sol b/contracts/stablecoins/endogenousAnchoredCoin/AlgorithmicStableCoin.sol new file mode 100644 index 0000000..1f7a78c --- /dev/null +++ b/contracts/stablecoins/endogenousAnchoredCoin/AlgorithmicStableCoin.sol @@ -0,0 +1,23 @@ +// https://blog.bitmex.com/wp-content/uploads/2018/06/A-Note-on-Cryptocurrency-Stabilisation-Seigniorage-Shares.pdf + +// This would be similar to a UST/LUNA + +// This is considered an Endogenous, Decentralized, Reflexive, Crypto Collateralized low volitility coin + +// Collateral: Endogenous +// Minting: Decentralized +// Value: Reflexive +// Collateral Type: Crypto + +// Sometimes just refered to as "Algorithmic Stablecoin" + +// But maybe a better name would be EndoDrCCoin (EndoDocCoin?), or maybe like "RobertsCoin" or something after the Sam Roberts paper... +// We should fix the name as a community + +pragma solidity ^0.8.7; + +contract AlgorithmicStableCoin { + +} + +// To be created... diff --git a/contracts/stablecoins/endogenousIndexCoin/Fpi.sol b/contracts/stablecoins/endogenousIndexCoin/Fpi.sol new file mode 100644 index 0000000..d97102a --- /dev/null +++ b/contracts/stablecoins/endogenousIndexCoin/Fpi.sol @@ -0,0 +1,3 @@ +// This would be similar to a FPI/FPIX + +// To be created... \ No newline at end of file diff --git a/contracts/stablecoins/exogenousAnchoredCoin/DSCEngine.sol b/contracts/stablecoins/exogenousAnchoredCoin/DSCEngine.sol new file mode 100644 index 0000000..a1d8e7f --- /dev/null +++ b/contracts/stablecoins/exogenousAnchoredCoin/DSCEngine.sol @@ -0,0 +1,293 @@ +// Based VEEEEEEEEEEERY LOOSELY on the MakerDAO DSS System (Dsc) +// Also has some Aave mixed in + +///////////////////////////////////////////////// +/****** We are ignoring the following modules: *********/ +// System Stabilizer: We are pretending that our liquidation model is good enough +// (It's definetly not) +// https://docs.makerdao.com/smart-contract-modules/system-stabilizer-module + +// Oracle Module: +// We use Chainlink instead +// https://docs.makerdao.com/smart-contract-modules/oracle-module + +// MKR Module: +// The MKR Module is for governance and a backstop against becoming insolvent. +// This is crucial for production +// https://docs.makerdao.com/smart-contract-modules/mkr-module + +// Governance Module: +// See above +// https://docs.makerdao.com/smart-contract-modules/governance-module + +// Rates Module: +// We are removing the rates module because we don't have governance +// We could include it more protection against insolvency, but we are going to pretend (again) that our liquidation thresholds are high enough +// https://docs.makerdao.com/smart-contract-modules/rates-module + +// Flash Mint Module +// Not necesary +// https://docs.makerdao.com/smart-contract-modules/flash-mint-module + +// Emergency Shutdown Module: +// Because +// https://docs.makerdao.com/smart-contract-modules/shutdown +///////////////////////////////////////////////// + +///////////////////////////////////////////////// +/****** Included Modules: *********/ + +// Core Module +// Collateral Module (but wrapped into one contract) +// Liquidation Module (but wrapped into one contract) + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.7; + +import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./DecentralizedStableCoin.sol"; +import "hardhat/console.sol"; + +error DSCEngine__TokenAddressesAndPriceFeedAddressesAmountsDontMatch(); +error DSCEngine__NeedsMoreThanZero(); +error DSCEngine__TokenNotAllowed(address token); +error DSCEngine__TransferFailed(); +error DSCEngine__BreaksHealthFactor(); +error DSCEngine__MintFailed(); +error DSCEngine__MustBreaksHealthFactor(); +error DSCEngine__HealthFactorOk(); + +contract DSCEngine is ReentrancyGuard { + uint256 public constant LIQUIDATION_THRESHOLD = 50; // This means you need to be 200% over-collateralized + uint256 public constant LIQUIDATION_BONUS = 10; // This means you get assets at a 10% discount when liquidating + uint256 public constant MIN_HEALTH_FACTOR = 1e18; + DecentralizedStableCoin public immutable i_dsc; + + mapping(address => address) public s_tokenAddressToPriceFeed; + // user -> token -> amount + mapping(address => mapping(address => uint256)) public s_userToTokenAddressToAmountDeposited; + // user -> amount + mapping(address => uint256) public s_userToDscMinted; + address[] public s_collateralTokens; + + event CollateralDeposited(address indexed user, uint256 indexed amount); + + modifier moreThanZero(uint256 amount) { + if (amount == 0) { + revert DSCEngine__NeedsMoreThanZero(); + } + _; + } + + modifier isAllowedToken(address token) { + if (s_tokenAddressToPriceFeed[token] == address(0)) { + revert DSCEngine__TokenNotAllowed(token); + } + _; + } + + constructor( + address[] memory tokenAddresses, + address[] memory priceFeedAddresses, + address dscAddress + ) { + if (tokenAddresses.length != priceFeedAddresses.length) { + revert DSCEngine__TokenAddressesAndPriceFeedAddressesAmountsDontMatch(); + } + // These feeds will be the USD pairs + // For example ETH / USD or MKR / USD + for (uint256 i = 0; i < tokenAddresses.length; i++) { + s_tokenAddressToPriceFeed[tokenAddresses[i]] = priceFeedAddresses[i]; + s_collateralTokens.push(tokenAddresses[i]); + } + // i_dsc = new DecentralizedStableCoin(); + i_dsc = DecentralizedStableCoin(dscAddress); + } + + function depositCollateralAndMintDsc( + address tokenCollateralAddress, + uint256 amountCollateral, + uint256 amountDscToMint + ) external { + despositCollateral(tokenCollateralAddress, amountCollateral); + mintDsc(amountDscToMint); + } + + function despositCollateral(address tokenCollateralAddress, uint256 amountCollateral) + public + moreThanZero(amountCollateral) + nonReentrant + isAllowedToken(tokenCollateralAddress) + { + s_userToTokenAddressToAmountDeposited[msg.sender][ + tokenCollateralAddress + ] += amountCollateral; + emit CollateralDeposited(msg.sender, amountCollateral); + bool success = IERC20(tokenCollateralAddress).transferFrom( + msg.sender, + address(this), + amountCollateral + ); + if (!success) { + revert DSCEngine__TransferFailed(); + } + } + + function redeemCollateralForDsc( + address tokenCollateralAddress, + uint256 amountCollateral, + uint256 amountDscToBurn + ) external { + burnDsc(amountDscToBurn); + redeemCollateral(tokenCollateralAddress, amountCollateral); + } + + function redeemCollateral(address tokenCollateralAddress, uint256 amountCollateral) + public + moreThanZero(amountCollateral) + nonReentrant + { + _redeemCollateral(tokenCollateralAddress, amountCollateral, msg.sender, msg.sender); + revertIfHealthFactorIsBroken(msg.sender); + } + + function _redeemCollateral( + address tokenCollateralAddress, + uint256 amountCollateral, + address from, + address to + ) private { + s_userToTokenAddressToAmountDeposited[from][tokenCollateralAddress] -= amountCollateral; + bool success = IERC20(tokenCollateralAddress).transfer(to, amountCollateral); + if (!success) { + revert DSCEngine__TransferFailed(); + } + } + + // Don't call this function directly, you will just lose money! + function burnDsc(uint256 amountDscToBurn) public moreThanZero(amountDscToBurn) nonReentrant { + _burnDsc(amountDscToBurn, msg.sender, msg.sender); + revertIfHealthFactorIsBroken(msg.sender); + } + + function _burnDsc( + uint256 amountDscToBurn, + address onBehalfOf, + address dscFrom + ) private { + s_userToDscMinted[onBehalfOf] -= amountDscToBurn; + bool success = i_dsc.transferFrom(dscFrom, address(this), amountDscToBurn); + if (!success) { + revert DSCEngine__TransferFailed(); + } + i_dsc.burn(amountDscToBurn); + } + + function mintDsc(uint256 amountDscToMint) public moreThanZero(amountDscToMint) nonReentrant { + s_userToDscMinted[msg.sender] += amountDscToMint; + revertIfHealthFactorIsBroken(msg.sender); + bool minted = i_dsc.mint(msg.sender, amountDscToMint); + if (minted != true) { + revert DSCEngine__MintFailed(); + } + } + + function getAccountInformation(address user) + public + view + returns (uint256 totalDscMinted, uint256 collateralValueInUsd) + { + totalDscMinted = s_userToDscMinted[user]; + collateralValueInUsd = getAccountCollateralValue(user); + } + + function healthFactor(address user) public view returns (uint256) { + (uint256 totalDscMinted, uint256 collateralValueInUsd) = getAccountInformation(user); + if (totalDscMinted == 0) return 100e18; + uint256 collateralAdjustedForThreshold = (collateralValueInUsd * LIQUIDATION_THRESHOLD) / + 100; + return (collateralAdjustedForThreshold * 1e18) / totalDscMinted; + } + + function getAccountCollateralValue(address user) + public + view + returns (uint256 totalCollateralValueInUsd) + { + for (uint256 index = 0; index < s_collateralTokens.length; index++) { + address token = s_collateralTokens[index]; + uint256 amount = s_userToTokenAddressToAmountDeposited[user][token]; + totalCollateralValueInUsd += getUsdValue(token, amount); + } + return totalCollateralValueInUsd; + } + + function getUsdValue(address token, uint256 amount) public view returns (uint256) { + AggregatorV3Interface priceFeed = AggregatorV3Interface(s_tokenAddressToPriceFeed[token]); + (, int256 price, , , ) = priceFeed.latestRoundData(); + // 1 ETH = 1000 USD + // The returned value from Chainlink will be 1000 * 1e8 + // Most USD pairs have 8 decimals, so we will just pretend they all do + // We want to have everything in terms of WEI, so we add 10 zeros at the end + return ((uint256(price) * 1e10) * amount) / 1e18; + + // 10.000000000000000000 ETH should be: + // 1,000.000000000000000000 USD + } + + function getTokenAmountFromUsd(address token, uint256 usdAmountInWei) + public + view + returns (uint256) + { + AggregatorV3Interface priceFeed = AggregatorV3Interface(s_tokenAddressToPriceFeed[token]); + (, int256 price, , , ) = priceFeed.latestRoundData(); + // 1 ETH = 1000 USD + // The returned value from Chainlink will be 1000 * 1e8 + // Most USD pairs have 8 decimals, so we will just pretend they all do + return (uint256(price) * 1e10 * 1e18) / usdAmountInWei; + } + + function revertIfHealthFactorIsBroken(address user) internal view { + uint256 userHealthFactor = healthFactor(user); + if (userHealthFactor < MIN_HEALTH_FACTOR) { + revert DSCEngine__BreaksHealthFactor(); + } + } + + function liquidate( + address collateral, + address user, + uint256 debtToCover + ) external { + uint256 startingUserHealthFactor = healthFactor(user); + if (startingUserHealthFactor >= MIN_HEALTH_FACTOR) { + revert DSCEngine__HealthFactorOk(); + } + uint256 tokenAmountFromDebtCovered = getTokenAmountFromUsd(collateral, debtToCover); + uint256 bonusCollateral = (tokenAmountFromDebtCovered * LIQUIDATION_BONUS) / 100; + // Burn DSC equal to debtToCover + // Figure out how much collateral to recover based on how much burnt + _redeemCollateral( + collateral, + tokenAmountFromDebtCovered + bonusCollateral, + user, + msg.sender + ); + _burnDsc(debtToCover, user, msg.sender); + + uint256 endingUserHealthFactor = healthFactor(user); + require(startingUserHealthFactor < endingUserHealthFactor); + } +} + +// Found this out by going through tenderly simulator for: +// https://dashboard.tenderly.co/tx/mainnet/0x89decb4ff427f63257f4679b3165f4a4f3701b79e9d29d383bd2565b5616bfb7/debugger?trace=0.0.0 + +// Calls openLockGemAndDraw(): which combines open, lockGem and draw on the DssProxyActions Contract +// open opens a new cdp (collateralized debt position) +// lockGem deposits collateral (moves LINK tokens) into this GemJoin contract: https://etherscan.io/address/0xdfccaf8fdbd2f4805c174f856a317765b49e4a50#readContract +// draw updates collateral fee rate and calls exit which gives Dsc to user diff --git a/contracts/stablecoins/exogenousAnchoredCoin/DecentralizedStableCoin.sol b/contracts/stablecoins/exogenousAnchoredCoin/DecentralizedStableCoin.sol new file mode 100644 index 0000000..dae08ce --- /dev/null +++ b/contracts/stablecoins/exogenousAnchoredCoin/DecentralizedStableCoin.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +// This is veeeeeeery loosely based off https://github.com/makerdao/dss (DAI) + +// This is considered an Exogenous, Decentralized, Anchored (pegged), Crypto Collateralized low volitility coin + +// Collateral: Exogenous +// Minting: Decentralized +// Value: Anchored (Pegged to USD) +// Collateral Type: Crypto + +// ExoDRCCoin... Which I'm going to call ExoDaCCoin... ExoDac? + +// Sometimes refered to just as "Crypto Collateralized Stablecoin" or "Decentralized Stablecoin" + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +error DecentralizedStableCoin__AmountMustBeMoreThanZero(); +error DecentralizedStableCoin__BurnAmountExceedsBalance(); +error DecentralizedStableCoin__NotZeroAddress(); + +contract DecentralizedStableCoin is ERC20Burnable, Ownable { + constructor() ERC20("DecentralizedStableCoin", "DSC") {} + + function burn(uint256 _amount) public override onlyOwner { + uint256 balance = balanceOf(msg.sender); + if (_amount <= 0) { + revert DecentralizedStableCoin__AmountMustBeMoreThanZero(); + } + if (balance < _amount) { + revert DecentralizedStableCoin__BurnAmountExceedsBalance(); + } + super.burn(_amount); + } + + function mint(address _to, uint256 _amount) external onlyOwner returns (bool) { + if (_to == address(0)) { + revert DecentralizedStableCoin__NotZeroAddress(); + } + if (_amount <= 0) { + revert DecentralizedStableCoin__AmountMustBeMoreThanZero(); + } + _mint(_to, _amount); + return true; + } +} diff --git a/contracts/stablecoins/exogenousIndexCoin/ReflexorIndex.sol b/contracts/stablecoins/exogenousIndexCoin/ReflexorIndex.sol new file mode 100644 index 0000000..acb0d81 --- /dev/null +++ b/contracts/stablecoins/exogenousIndexCoin/ReflexorIndex.sol @@ -0,0 +1,4 @@ +// This would be similar to a RAI +// https://reflexer.finance/ + +// To be created... \ No newline at end of file diff --git a/deploy/00-deploy-mocks.js b/deploy/00-deploy-mocks.js index 741dc2d..f0f3c45 100644 --- a/deploy/00-deploy-mocks.js +++ b/deploy/00-deploy-mocks.js @@ -4,6 +4,12 @@ const { ethers } = require("hardhat") const DAI_INITIAL_PRICE = ethers.utils.parseEther("0.001") // 1 DAI = $1 & ETH = $1,000 const BTC_INITIAL_PRICE = ethers.utils.parseEther("2") // 1 WBTC = $2,000 & ETH = $1,000 const DECIMALS = 18 +const USD_DECIMALS = 18 + +// We use 8 decimals for USD based currencies +const ETH_USD_INITIAL_PRICE = ethers.utils.parseUnits("1000", 8) // 1 ETH = $1,000 +const BTC_USD_INITIAL_PRICE = ethers.utils.parseUnits("2000", 8) // 1 BTC = $2,000 +const DAI_USD_INITIAL_PRICE = ethers.utils.parseUnits("1", 8) // 1 DAI = $1 module.exports = async ({ getNamedAccounts, deployments }) => { const { deploy, log } = deployments @@ -36,12 +42,38 @@ module.exports = async ({ getNamedAccounts, deployments }) => { log: true, args: ["Wrapped Bitcoin", "WBTC"], }) + await deploy("WETH", { + contract: "MockERC20", + from: deployer, + log: true, + args: ["Wrapped Ethereum", "WETH"], + }) await deploy("RandomToken", { contract: "MockERC20", from: deployer, log: true, args: ["Random Token", "RT"], }) + + // For stablecoins + await deploy("ETHUSDPriceFeed", { + contract: "MockV3Aggregator", + from: deployer, + log: true, + args: [USD_DECIMALS, ETH_USD_INITIAL_PRICE], + }) + await deploy("BTCUSDPriceFeed", { + contract: "MockV3Aggregator", + from: deployer, + log: true, + args: [USD_DECIMALS, BTC_USD_INITIAL_PRICE], + }) + await deploy("DAIUSDPriceFeed", { + contract: "MockV3Aggregator", + from: deployer, + log: true, + args: [USD_DECIMALS, DAI_USD_INITIAL_PRICE], + }) log("Mocks Deployed!") log("----------------------------------------------------") log("You are deploying to a local network, you'll need a local network running to interact") diff --git a/deploy/05-deploy-stablecoins.js b/deploy/05-deploy-centralized-stablecoins.js similarity index 100% rename from deploy/05-deploy-stablecoins.js rename to deploy/05-deploy-centralized-stablecoins.js diff --git a/deploy/06-deploy-decentralized-stablecoin.js b/deploy/06-deploy-decentralized-stablecoin.js new file mode 100644 index 0000000..c8971a4 --- /dev/null +++ b/deploy/06-deploy-decentralized-stablecoin.js @@ -0,0 +1,77 @@ +const { network, ethers } = require("hardhat") +const { + developmentChains, + VERIFICATION_BLOCK_CONFIRMATIONS, + networkConfig, +} = require("../helper-hardhat-config") +const { verify } = require("../helper-functions") + +module.exports = async ({ getNamedAccounts, deployments }) => { + const { deploy, log } = deployments + const { deployer } = await getNamedAccounts() + const waitBlockConfirmations = developmentChains.includes(network.name) + ? 1 + : VERIFICATION_BLOCK_CONFIRMATIONS + log("----------------------------------------------------") + const dscArgs = [] + const decentralizedStablecoinDeployment = await deploy("DecentralizedStableCoin", { + from: deployer, + args: dscArgs, + log: true, + waitConfirmations: waitBlockConfirmations, + }) + + let tokenAddresses, priceFeedAddresses, dsceArgs + + if (developmentChains.includes(network.name)) { + const dai = await ethers.getContract("DAI") + const wbtc = await ethers.getContract("WBTC") + const weth = await ethers.getContract("WETH") + const daiUsdPriceFeed = await ethers.getContract("DAIUSDPriceFeed") + const btcUsdPriceFeed = await ethers.getContract("BTCUSDPriceFeed") + const ethUsdPriceFeed = await ethers.getContract("ETHUSDPriceFeed") + + tokenAddresses = [dai.address, wbtc.address, weth.address] + priceFeedAddresses = [ + daiUsdPriceFeed.address, + btcUsdPriceFeed.address, + ethUsdPriceFeed.address, + ] + + dsceArgs = [tokenAddresses, priceFeedAddresses, decentralizedStablecoinDeployment.address] + } else { + const tokenAddress = [ + networkConfig[network.config.chainId]["dai"], + networkConfig[network.config.chainId]["wbtc"], + networkConfig[network.config.chainId]["weth"], + ] + const priceFeedAddresses = [ + networkConfig[network.config.chainId]["daiUsdPriceFeed"], + networkConfig[network.config.chainId]["btcUsdPriceFeed"], + networkConfig[network.config.chainId]["ethUsdPriceFeed"], + ] + dsceArgs = [tokenAddress, priceFeedAddresses, decentralizedStablecoinDeployment.address] + } + + const dscEngineDeployment = await deploy("DSCEngine", { + from: deployer, + args: dsceArgs, + log: true, + waitConfirmations: waitBlockConfirmations, + }) + + const dsc = await ethers.getContract("DecentralizedStableCoin") + await dsc.transferOwnership(dscEngineDeployment.address) + + // Verify the deployment + if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) { + log("Verifying DSC...") + await verify(decentralizedStablecoinDeployment.address, dscArgs) + + log("Verifying DSCE...") + await verify(dscEngineDeployment.address, dsceArgs) + } + log("----------------------------------------------------") +} + +module.exports.tags = ["all", "decentralizedstablecoin"] diff --git a/helper-hardhat-config.js b/helper-hardhat-config.js index b531b51..978ac3d 100644 --- a/helper-hardhat-config.js +++ b/helper-hardhat-config.js @@ -13,6 +13,10 @@ const networkConfig = { wbtcEthPriceFeed: "0x2431452A0010a43878bF198e170F6319Af6d27F4", dai: "0xFab46E002BbF0b4509813474841E0716E6730136", // https://erc20faucet.com/ wbtc: "0x577D296678535e4903D59A4C929B718e1D575e0A", // https://rinkeby.etherscan.io/token/0x577d296678535e4903d59a4c929b718e1d575e0a#writeContract + weth: "0xc778417E063141139Fce010982780140Aa0cD5Ab", + daiUsdPriceFeed: "0x2bA49Aaa16E6afD2a993473cfB70Fa8559B523cF", + btcUsdPriceFeed: "0xECe365B379E1dD183B20fc5f022230C044d51404", + ethUsdPriceFeed: "0x8A753747A1Fa494EC906cE90E9f37563A8AF630e", }, } diff --git a/test/unit/centralizedStablecoin.test.js b/test/unit/centralizedStablecoin.test.js deleted file mode 100644 index bb71296..0000000 --- a/test/unit/centralizedStablecoin.test.js +++ /dev/null @@ -1,30 +0,0 @@ -const { assert, expect } = require("chai") -const { network, deployments, ethers } = require("hardhat") -const { developmentChains } = require("../../helper-hardhat-config") - -!developmentChains.includes(network.name) - ? describe.skip - : describe("Centralized Stablecoin tests", function () { - let centralizedStablecoin, deployer, accounts - beforeEach(async () => { - accounts = await ethers.getSigners() - deployer = accounts[0] - await deployments.fixture(["centralizedstablecoin"]) - centralizedStablecoin = await ethers.getContract("CentralizedStableCoin") - }) - - it("Can blacklist", async function () { - const transferAmount = ethers.utils.parseUnits("1", "ether") - const blackListedAccount = accounts[1] - const blacklistTx = await centralizedStablecoin.blacklist(blackListedAccount.address) - await blacklistTx.wait(1) - await expect( - centralizedStablecoin.transfer(blackListedAccount.address, transferAmount) - ).to.be.revertedWith("CentralizedStablecoin__AddressBlacklisted()") - }) - - // incomplete - // it("Can mint", async function () {}) - // it("Can burn", async function () {}) - // it("only owner can assign minters and blacklist", async function () {}) - }) diff --git a/test/unit/stablecoins/centralizedStableCoin.test.js b/test/unit/stablecoins/centralizedStableCoin.test.js new file mode 100644 index 0000000..c1a1ea5 --- /dev/null +++ b/test/unit/stablecoins/centralizedStableCoin.test.js @@ -0,0 +1,71 @@ +const { assert, expect } = require("chai") +const { network, deployments, ethers } = require("hardhat") +const { developmentChains } = require("../../../helper-hardhat-config") + +!developmentChains.includes(network.name) + ? describe.skip + : describe("Centralized Stablecoin tests", function () { + let centralizedStableCoin, deployer, accounts, badActor + beforeEach(async () => { + accounts = await ethers.getSigners() + deployer = accounts[0] + badActor = accounts[1] + await deployments.fixture(["centralizedstablecoin"]) + centralizedStableCoin = await ethers.getContract("CentralizedStableCoin") + }) + + it("Can blacklist", async function () { + const transferAmount = ethers.utils.parseUnits("1", "ether") + const blackListedAccount = accounts[1] + const blacklistTx = await centralizedStableCoin.blacklist(blackListedAccount.address) + await blacklistTx.wait(1) + await expect( + centralizedStableCoin.transfer(blackListedAccount.address, transferAmount) + ).to.be.revertedWith("CentralizedStableCoin__AddressBlacklisted()") + }) + + it("allows minters to mint", async function () { + const startingMinterBalance = await centralizedStableCoin.balanceOf(deployer.address) + + const mintAmount = ethers.utils.parseUnits("100", "ether") + const configureMintTx = await centralizedStableCoin.configureMinter( + deployer.address, + mintAmount + ) + await configureMintTx.wait(1) + + const mintTx = await centralizedStableCoin.mint(deployer.address, mintAmount) + await mintTx.wait(1) + + const endingMinterBalance = await centralizedStableCoin.balanceOf(deployer.address) + assert( + endingMinterBalance.sub(startingMinterBalance).toString() == mintAmount.toString() + ) + }) + it("doesn't allow non-minters to mint", async function () { + const mintAmount = ethers.utils.parseUnits("100", "ether") + await centralizedStableCoin.connect(badActor) + await expect( + centralizedStableCoin.mint(deployer.address, mintAmount) + ).to.be.revertedWith("CentralizedStableCoin__NotMinter()") + }) + it("Can burn", async function () { + // Arrange + const startingBalance = await centralizedStableCoin.balanceOf(deployer.address) + const burnAmount = ethers.utils.parseUnits("1", "ether") + const configureBurnTx = await centralizedStableCoin.configureMinter( + deployer.address, + burnAmount + ) + await configureBurnTx.wait(1) + + // Act + const burnTx = await centralizedStableCoin.burn(burnAmount) + await burnTx.wait(1) + + // Assert + const endingBalance = await centralizedStableCoin.balanceOf(deployer.address) + assert(startingBalance.sub(burnAmount).toString() == endingBalance.toString()) + }) + // More tests below... + }) diff --git a/test/unit/stablecoins/decentralizedStableCoin.test.js b/test/unit/stablecoins/decentralizedStableCoin.test.js new file mode 100644 index 0000000..c525ae6 --- /dev/null +++ b/test/unit/stablecoins/decentralizedStableCoin.test.js @@ -0,0 +1,114 @@ +const { assert, expect } = require("chai") +const { network, deployments, ethers } = require("hardhat") +const { developmentChains } = require("../../../helper-hardhat-config") + +!developmentChains.includes(network.name) + ? describe.skip + : describe("Decentralized Stablecoin tests", function () { + let decentralizedStableCoin, + deployer, + accounts, + liquidator, + dscEngine, + weth, + ethUsdPriceFeed + beforeEach(async () => { + accounts = await ethers.getSigners() + deployer = accounts[0] + liquidator = accounts[1] + await deployments.fixture(["mocks", "decentralizedstablecoin"]) + decentralizedStableCoin = await ethers.getContract("DecentralizedStableCoin") + dscEngine = await ethers.getContract("DSCEngine") + weth = await ethers.getContract("WETH") + ethUsdPriceFeed = await ethers.getContract("ETHUSDPriceFeed") + }) + + it("can be minted with deposited collateral", async function () { + const amountCollateral = ethers.utils.parseEther("10") // Price Starts off at $1,000 + const amountToMint = ethers.utils.parseEther("100") // $100 minted with $10,000 collateral + await weth.approve(dscEngine.address, amountCollateral) + + await dscEngine.depositCollateralAndMintDsc( + weth.address, + amountCollateral, + amountToMint + ) + + const balance = await decentralizedStableCoin.balanceOf(deployer.address) + + assert.equal(balance.toString(), amountToMint.toString()) + }) + + it("can redeem deposited collateral", async function () { + const amountCollateral = ethers.utils.parseEther("10") // Price Starts off at $1,000 + const amountToMint = ethers.utils.parseEther("100") // $100 minted with $10,000 collateral + await weth.approve(dscEngine.address, amountCollateral) + + await dscEngine.depositCollateralAndMintDsc( + weth.address, + amountCollateral, + amountToMint + ) + await decentralizedStableCoin.approve(dscEngine.address, amountToMint) + await dscEngine.redeemCollateralForDsc(weth.address, amountCollateral, amountToMint) + + assert(await decentralizedStableCoin.balanceOf(dscEngine.address), "0") + }) + + it("properly reports health factor", async function () { + const amountCollateral = ethers.utils.parseEther("10") // Price Starts off at $1,000 + const amountToMint = ethers.utils.parseEther("100") // $100 minted with $10,000 collateral + await weth.approve(dscEngine.address, amountCollateral) + + await dscEngine.depositCollateralAndMintDsc( + weth.address, + amountCollateral, + amountToMint + ) + + const healthFactor = await dscEngine.healthFactor(deployer.address) + // $100 minted with $10,000 collateral at a 50% liquidation threshold means that + // We must have $200 collateral at all times + // Which means, 10,000 / 200 = 50 health factor + assert.equal(ethers.utils.formatEther(healthFactor.toString()), "50.0") + }) + + it("can be liquidated if pricing changes", async function () { + const amountCollateral = ethers.utils.parseEther("10") // Price Starts off at $1,000 + const amountToMint = ethers.utils.parseEther("100") // $100 minted with $10,000 collateral + await weth.approve(dscEngine.address, amountCollateral) + await dscEngine.depositCollateralAndMintDsc( + weth.address, + amountCollateral, + amountToMint + ) + + const ethUsdUpdatedPrice = ethers.utils.parseUnits("18", 8) // 1 ETH = $18, meaning we are way under 200% collateralization + + const updateTx = await ethUsdPriceFeed.updateAnswer(ethUsdUpdatedPrice) + await updateTx.wait() + + const healthFactor = (await dscEngine.healthFactor(deployer.address)).toString() + assert.equal(ethers.utils.formatEther(healthFactor), "0.9") + // Uh oh! This means we can liquidate! + + // Let's give our liquidator some DSC + const moreCollateral = ethers.utils.parseEther("1000") + await weth.transfer(liquidator.address, moreCollateral) + const liquidatorConnectedWeth = await weth.connect(liquidator) + const liquidatorConnectedDsce = await dscEngine.connect(liquidator) + const liquidatorConnectedDsc = await decentralizedStableCoin.connect(liquidator) + + await liquidatorConnectedWeth.approve(dscEngine.address, moreCollateral) + await liquidatorConnectedDsce.depositCollateralAndMintDsc( + weth.address, + moreCollateral, + amountToMint + ) + + const balance = await decentralizedStableCoin.balanceOf(liquidator.address) + assert.equal(balance.toString(), amountToMint.toString()) + await liquidatorConnectedDsc.approve(dscEngine.address, amountToMint) + await liquidatorConnectedDsce.liquidate(weth.address, deployer.address, amountToMint) + }) + })