├── .commitlintrc.js ├── .czrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.yaml ├── .gitattributes ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc.yaml ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.0.0.cjs ├── .yarnrc.yml ├── README.md ├── README2.md ├── contracts ├── Loot.sol ├── MockERC20.sol ├── MockERC721.sol ├── NftStake.sol ├── Paper.sol ├── governance │ ├── DopeDAO.sol │ └── Timelock.sol └── test │ ├── DopeDAO.sol │ └── Receiver.sol ├── hardhat.config.ts ├── package.json ├── scripts ├── daorun.ts └── dip6.ts ├── tasks ├── accounts.ts ├── clean.ts └── deployers │ ├── index.ts │ └── nftStake.ts ├── test ├── NftStake.behavior.ts ├── NftStake.ts ├── governance │ └── DopeDAO.ts └── types.ts ├── tsconfig.json ├── utils ├── constants.ts └── network.ts └── yarn.lock /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # network specific node uri : `"ETH_NODE_URI_" + networkName.toUpperCase()` 2 | ETH_NODE_URI_MAINNET=https://eth-mainnet.alchemyapi.io/v2/ 3 | # generic node uri (if no specific found) : 4 | ETH_NODE_URI=https://{{networkName}}.infura.io/v3/ 5 | 6 | # network specific mnemonic : `"MNEMONIC_ " + networkName.toUpperCase()` 7 | MNEMONIC_MAINNET= 8 | # generic mnemonic (if no specific found): 9 | MNEMONIC= 10 | 11 | # coinmarketcap api key for gas report 12 | COINMARKETCAP_API_KEY= 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | 11 | # files 12 | .solcover.js 13 | coverage.json 14 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "eslint:recommended" 3 | - "plugin:@typescript-eslint/eslint-recommended" 4 | - "plugin:@typescript-eslint/recommended" 5 | - "prettier" 6 | parser: "@typescript-eslint/parser" 7 | parserOptions: 8 | project: "tsconfig.json" 9 | plugins: 10 | - "@typescript-eslint" 11 | root: true 12 | rules: 13 | "@typescript-eslint/no-floating-promises": 14 | - error 15 | - ignoreIIFE: true 16 | ignoreVoid: true 17 | "@typescript-eslint/no-inferrable-types": "off" 18 | "@typescript-eslint/no-unused-vars": 19 | - error 20 | - argsIgnorePattern: _ 21 | varsIgnorePattern: _ 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | contracts: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: "14.x" 16 | - uses: actions/cache@v2 17 | with: 18 | path: "**/node_modules" 19 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 20 | - run: yarn 21 | - run: yarn typechain 22 | env: 23 | INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} 24 | MNEMONIC: ${{ secrets.MNEMONIC }} 25 | - run: yarn test 26 | env: 27 | INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} 28 | MNEMONIC: ${{ secrets.MNEMONIC }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .coverage_artifacts/ 3 | .coverage_cache/ 4 | .coverage_contracts/ 5 | .yarn/* 6 | !.yarn/patches 7 | !.yarn/releases 8 | !.yarn/plugins 9 | !.yarn/sdks 10 | !.yarn/versions 11 | artifacts/ 12 | build/ 13 | cache/ 14 | coverage/ 15 | dist/ 16 | lib/ 17 | node_modules/ 18 | typechain/ 19 | 20 | # files 21 | *.env 22 | *.log 23 | *.tsbuildinfo 24 | coverage.json 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn dlx commitlint -e 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn dlx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,json,md,sol,ts}": [ 3 | "prettier --config ./.prettierrc.yaml --write" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | 11 | # files 12 | coverage.json 13 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | arrowParens: avoid 2 | bracketSpacing: true 3 | endOfLine: auto 4 | printWidth: 120 5 | singleQuote: false 6 | tabWidth: 2 7 | trailingComma: all 8 | 9 | overrides: 10 | - files: "*.sol" 11 | options: 12 | tabWidth: 4 13 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const shell = require("shelljs"); 2 | 3 | // The environment variables are loaded in hardhat.config.ts 4 | const mnemonic = process.env.MNEMONIC; 5 | if (!mnemonic) { 6 | throw new Error("Please set your MNEMONIC in a .env file"); 7 | } 8 | 9 | module.exports = { 10 | istanbulReporter: ["html", "lcov"], 11 | onCompileComplete: async function (_config) { 12 | await run("typechain"); 13 | }, 14 | onIstanbulComplete: async function (_config) { 15 | // We need to do this because solcover generates bespoke artifacts. 16 | shell.rm("-rf", "./artifacts"); 17 | // shell.rm("-rf", "./typechain"); 18 | }, 19 | providerOptions: { 20 | mnemonic, 21 | }, 22 | skipFiles: ["mocks", "test"], 23 | }; 24 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 8], 6 | "compiler-version": ["error", ">=0.8.4"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "max-line-length": ["error", 120], 11 | "not-rely-on-time": "off", 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "endOfLine": "auto" 16 | } 17 | ], 18 | "reason-string": ["warn", { "maxLength": 64 }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .yarn/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.0.0.cjs 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOPEDAO contracts 2 | 3 | ## Development 4 | 5 | ### Pre Requisites 6 | 7 | Before running any command, you need to create a `.env` file and set a BIP-39 compatible mnemonic as an environment 8 | variable. Follow the example in `.env.example`. If you don't already have a mnemonic, use this [website](https://iancoleman.io/bip39/) to generate one. 9 | 10 | Then, proceed with installing dependencies: 11 | 12 | ```sh 13 | yarn install 14 | ``` 15 | 16 | ### Compile 17 | 18 | Compile the smart contracts with Hardhat: 19 | 20 | ```sh 21 | $ yarn compile 22 | ``` 23 | 24 | ### TypeChain 25 | 26 | Compile the smart contracts and generate TypeChain artifacts: 27 | 28 | ```sh 29 | $ yarn typechain 30 | ``` 31 | 32 | ### Lint Solidity 33 | 34 | Lint the Solidity code: 35 | 36 | ```sh 37 | $ yarn lint:sol 38 | ``` 39 | 40 | ### Lint TypeScript 41 | 42 | Lint the TypeScript code: 43 | 44 | ```sh 45 | $ yarn lint:ts 46 | ``` 47 | 48 | ### Test 49 | 50 | Run the Mocha tests: 51 | 52 | ```sh 53 | $ yarn test 54 | ``` 55 | 56 | ### Coverage 57 | 58 | Generate the code coverage report: 59 | 60 | ```sh 61 | $ yarn coverage 62 | ``` 63 | 64 | ### Report Gas 65 | 66 | See the gas usage per unit test and average gas per method call: 67 | 68 | ```sh 69 | $ REPORT_GAS=true yarn test 70 | ``` 71 | 72 | ### Clean 73 | 74 | Delete the smart contract artifacts, the coverage reports and the Hardhat cache: 75 | 76 | ```sh 77 | $ yarn clean 78 | ``` 79 | 80 | ### Deploy 81 | 82 | Deploy the contracts to Hardhat Network: 83 | 84 | ```sh 85 | $ yarn deploy --greeting "Bonjour, le monde!" 86 | ``` 87 | 88 | ## Syntax Highlighting 89 | 90 | If you use VSCode, you can enjoy syntax highlighting for your Solidity code via the 91 | [vscode-solidity](https://github.com/juanfranblanco/vscode-solidity) extension. The recommended approach to set the 92 | compiler version is to add the following fields to your VSCode user settings: 93 | 94 | ```json 95 | { 96 | "solidity.compileUsingRemoteVersion": "v0.8.4+commit.c7e474f2", 97 | "solidity.defaultCompiler": "remote" 98 | } 99 | ``` 100 | 101 | Where of course `v0.8.4+commit.c7e474f2` can be replaced with any other version. 102 | -------------------------------------------------------------------------------- /README2.md: -------------------------------------------------------------------------------- 1 | Deployment task: 2 | 3 | Deploy for DOPE 4 | 5 | npx hardhat deploy:NFTStake --nft 0x8707276df042e89669d69a177d3da7dc78bd8723 --erc20 0x8390756AbF18f752744Eef2AF8eE745bD499b23b --dao 0xB20adB7Aa32361Dadfdeb87Dd6A072e7B75A7b59 --reward <<<<<>>>> --network mainnet 6 | -------------------------------------------------------------------------------- /contracts/Loot.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 4 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | abstract contract ERC721Checkpointable is ERC721Enumerable { 8 | /// @notice Defines decimals as per ERC-20 convention to make integrations with 3rd party governance platforms easier 9 | uint8 public constant decimals = 0; 10 | 11 | /// @notice A record of each accounts delegate 12 | mapping(address => address) private _delegates; 13 | 14 | /// @notice A checkpoint for marking number of votes from a given block 15 | struct Checkpoint { 16 | uint32 fromBlock; 17 | uint96 votes; 18 | } 19 | 20 | /// @notice A record of votes checkpoints for each account, by index 21 | mapping(address => mapping(uint32 => Checkpoint)) public checkpoints; 22 | 23 | /// @notice The number of checkpoints for each account 24 | mapping(address => uint32) public numCheckpoints; 25 | 26 | /// @notice The EIP-712 typehash for the contract's domain 27 | bytes32 public constant DOMAIN_TYPEHASH = 28 | keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); 29 | 30 | /// @notice The EIP-712 typehash for the delegation struct used by the contract 31 | bytes32 public constant DELEGATION_TYPEHASH = 32 | keccak256('Delegation(address delegatee,uint256 nonce,uint256 expiry)'); 33 | 34 | /// @notice A record of states for signing / validating signatures 35 | mapping(address => uint256) public nonces; 36 | 37 | /// @notice An event thats emitted when an account changes its delegate 38 | event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); 39 | 40 | /// @notice An event thats emitted when a delegate account's vote balance changes 41 | event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); 42 | 43 | /** 44 | * @notice The votes a delegator can delegate, which is the current balance of the delegator. 45 | * @dev Used when calling `_delegate()` 46 | */ 47 | function votesToDelegate(address delegator) public view returns (uint96) { 48 | return safe96(balanceOf(delegator), 'ERC721Checkpointable::votesToDelegate: amount exceeds 96 bits'); 49 | } 50 | 51 | /** 52 | * @notice Overrides the standard `Comp.sol` delegates mapping to return 53 | * the delegator's own address if they haven't delegated. 54 | * This avoids having to delegate to oneself. 55 | */ 56 | function delegates(address delegator) public view returns (address) { 57 | address current = _delegates[delegator]; 58 | return current == address(0) ? delegator : current; 59 | } 60 | 61 | /** 62 | * @notice Adapted from `_transferTokens()` in `Comp.sol` to update delegate votes. 63 | * @dev hooks into OpenZeppelin's `ERC721._transfer` 64 | */ 65 | function _beforeTokenTransfer( 66 | address from, 67 | address to, 68 | uint256 tokenId 69 | ) internal override { 70 | super._beforeTokenTransfer(from, to, tokenId); 71 | 72 | /// @notice Differs from `_transferTokens()` to use `delegates` override method to simulate auto-delegation 73 | _moveDelegates(delegates(from), delegates(to), 1); 74 | } 75 | 76 | /** 77 | * @notice Delegate votes from `msg.sender` to `delegatee` 78 | * @param delegatee The address to delegate votes to 79 | */ 80 | function delegate(address delegatee) public { 81 | if (delegatee == address(0)) delegatee = msg.sender; 82 | return _delegate(msg.sender, delegatee); 83 | } 84 | 85 | /** 86 | * @notice Delegates votes from signatory to `delegatee` 87 | * @param delegatee The address to delegate votes to 88 | * @param nonce The contract state required to match the signature 89 | * @param expiry The time at which to expire the signature 90 | * @param v The recovery byte of the signature 91 | * @param r Half of the ECDSA signature pair 92 | * @param s Half of the ECDSA signature pair 93 | */ 94 | function delegateBySig( 95 | address delegatee, 96 | uint256 nonce, 97 | uint256 expiry, 98 | uint8 v, 99 | bytes32 r, 100 | bytes32 s 101 | ) public { 102 | bytes32 domainSeparator = keccak256( 103 | abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name())), getChainId(), address(this)) 104 | ); 105 | bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry)); 106 | bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); 107 | address signatory = ecrecover(digest, v, r, s); 108 | require(signatory != address(0), 'ERC721Checkpointable::delegateBySig: invalid signature'); 109 | require(nonce == nonces[signatory]++, 'ERC721Checkpointable::delegateBySig: invalid nonce'); 110 | require(block.timestamp <= expiry, 'ERC721Checkpointable::delegateBySig: signature expired'); 111 | return _delegate(signatory, delegatee); 112 | } 113 | 114 | /** 115 | * @notice Gets the current votes balance for `account` 116 | * @param account The address to get votes balance 117 | * @return The number of current votes for `account` 118 | */ 119 | function getCurrentVotes(address account) external view returns (uint96) { 120 | uint32 nCheckpoints = numCheckpoints[account]; 121 | return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; 122 | } 123 | 124 | /** 125 | * @notice Determine the prior number of votes for an account as of a block number 126 | * @dev Block number must be a finalized block or else this function will revert to prevent misinformation. 127 | * @param account The address of the account to check 128 | * @param blockNumber The block number to get the vote balance at 129 | * @return The number of votes the account had as of the given block 130 | */ 131 | function getPriorVotes(address account, uint256 blockNumber) public view returns (uint96) { 132 | require(blockNumber < block.number, 'ERC721Checkpointable::getPriorVotes: not yet determined'); 133 | 134 | uint32 nCheckpoints = numCheckpoints[account]; 135 | if (nCheckpoints == 0) { 136 | return 0; 137 | } 138 | 139 | // First check most recent balance 140 | if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { 141 | return checkpoints[account][nCheckpoints - 1].votes; 142 | } 143 | 144 | // Next check implicit zero balance 145 | if (checkpoints[account][0].fromBlock > blockNumber) { 146 | return 0; 147 | } 148 | 149 | uint32 lower = 0; 150 | uint32 upper = nCheckpoints - 1; 151 | while (upper > lower) { 152 | uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow 153 | Checkpoint memory cp = checkpoints[account][center]; 154 | if (cp.fromBlock == blockNumber) { 155 | return cp.votes; 156 | } else if (cp.fromBlock < blockNumber) { 157 | lower = center; 158 | } else { 159 | upper = center - 1; 160 | } 161 | } 162 | return checkpoints[account][lower].votes; 163 | } 164 | 165 | function _delegate(address delegator, address delegatee) internal { 166 | /// @notice differs from `_delegate()` in `Comp.sol` to use `delegates` override method to simulate auto-delegation 167 | address currentDelegate = delegates(delegator); 168 | 169 | _delegates[delegator] = delegatee; 170 | 171 | emit DelegateChanged(delegator, currentDelegate, delegatee); 172 | 173 | uint96 amount = votesToDelegate(delegator); 174 | 175 | _moveDelegates(currentDelegate, delegatee, amount); 176 | } 177 | 178 | function _moveDelegates( 179 | address srcRep, 180 | address dstRep, 181 | uint96 amount 182 | ) internal { 183 | if (srcRep != dstRep && amount > 0) { 184 | if (srcRep != address(0)) { 185 | uint32 srcRepNum = numCheckpoints[srcRep]; 186 | uint96 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; 187 | uint96 srcRepNew = sub96(srcRepOld, amount, 'ERC721Checkpointable::_moveDelegates: amount underflows'); 188 | _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); 189 | } 190 | 191 | if (dstRep != address(0)) { 192 | uint32 dstRepNum = numCheckpoints[dstRep]; 193 | uint96 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0; 194 | uint96 dstRepNew = add96(dstRepOld, amount, 'ERC721Checkpointable::_moveDelegates: amount overflows'); 195 | _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); 196 | } 197 | } 198 | } 199 | 200 | function _writeCheckpoint( 201 | address delegatee, 202 | uint32 nCheckpoints, 203 | uint96 oldVotes, 204 | uint96 newVotes 205 | ) internal { 206 | uint32 blockNumber = safe32( 207 | block.number, 208 | 'ERC721Checkpointable::_writeCheckpoint: block number exceeds 32 bits' 209 | ); 210 | 211 | if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) { 212 | checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; 213 | } else { 214 | checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes); 215 | numCheckpoints[delegatee] = nCheckpoints + 1; 216 | } 217 | 218 | emit DelegateVotesChanged(delegatee, oldVotes, newVotes); 219 | } 220 | 221 | function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { 222 | require(n < 2**32, errorMessage); 223 | return uint32(n); 224 | } 225 | 226 | function safe96(uint256 n, string memory errorMessage) internal pure returns (uint96) { 227 | require(n < 2**96, errorMessage); 228 | return uint96(n); 229 | } 230 | 231 | function add96( 232 | uint96 a, 233 | uint96 b, 234 | string memory errorMessage 235 | ) internal pure returns (uint96) { 236 | uint96 c = a + b; 237 | require(c >= a, errorMessage); 238 | return c; 239 | } 240 | 241 | function sub96( 242 | uint96 a, 243 | uint96 b, 244 | string memory errorMessage 245 | ) internal pure returns (uint96) { 246 | require(b <= a, errorMessage); 247 | return a - b; 248 | } 249 | 250 | function getChainId() internal view returns (uint256) { 251 | uint256 chainId; 252 | assembly { 253 | chainId := chainid() 254 | } 255 | return chainId; 256 | } 257 | } 258 | 259 | library Base64 { 260 | bytes internal constant TABLE = 261 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 262 | 263 | /// @notice Encodes some bytes to the base64 representation 264 | function encode(bytes memory data) internal pure returns (string memory) { 265 | uint256 len = data.length; 266 | if (len == 0) return ""; 267 | 268 | // multiply by 4/3 rounded up 269 | uint256 encodedLen = 4 * ((len + 2) / 3); 270 | 271 | // Add some extra buffer at the end 272 | bytes memory result = new bytes(encodedLen + 32); 273 | 274 | bytes memory table = TABLE; 275 | 276 | assembly { 277 | let tablePtr := add(table, 1) 278 | let resultPtr := add(result, 32) 279 | 280 | for { 281 | let i := 0 282 | } lt(i, len) { 283 | 284 | } { 285 | i := add(i, 3) 286 | let input := and(mload(add(data, i)), 0xffffff) 287 | 288 | let out := mload(add(tablePtr, and(shr(18, input), 0x3F))) 289 | out := shl(8, out) 290 | out := add( 291 | out, 292 | and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF) 293 | ) 294 | out := shl(8, out) 295 | out := add( 296 | out, 297 | and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF) 298 | ) 299 | out := shl(8, out) 300 | out := add( 301 | out, 302 | and(mload(add(tablePtr, and(input, 0x3F))), 0xFF) 303 | ) 304 | out := shl(224, out) 305 | 306 | mstore(resultPtr, out) 307 | 308 | resultPtr := add(resultPtr, 4) 309 | } 310 | 311 | switch mod(len, 3) 312 | case 1 { 313 | mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) 314 | } 315 | case 2 { 316 | mstore(sub(resultPtr, 1), shl(248, 0x3d)) 317 | } 318 | 319 | mstore(result, encodedLen) 320 | } 321 | 322 | return string(result); 323 | } 324 | } 325 | 326 | contract DopeWarsLoot is ERC721Checkpointable, ReentrancyGuard, Ownable { 327 | string[] private weapons = [ 328 | "Pocket Knife", 329 | "Chain", 330 | "Knife", 331 | "Crowbar", 332 | "Handgun", 333 | "AK47", 334 | "Shovel", 335 | "Baseball Bat", 336 | "Tire Iron", 337 | "Police Baton", 338 | "Pepper Spray", 339 | "Razor Blade", 340 | "Chain", 341 | "Taser", 342 | "Brass Knuckles", 343 | "Shotgun", 344 | "Glock", 345 | "Uzi" 346 | ]; 347 | 348 | string[] private clothes = [ 349 | "White T Shirt", 350 | "Black T Shirt", 351 | "White Hoodie", 352 | "Black Hoodie", 353 | "Bulletproof Vest", 354 | "3 Piece Suit", 355 | "Checkered Shirt", 356 | "Bikini", 357 | "Golden Shirt", 358 | "Leather Vest", 359 | "Blood Stained Shirt", 360 | "Police Uniform", 361 | "Combat Jacket", 362 | "Basketball Jersey", 363 | "Track Suit", 364 | "Trenchcoat", 365 | "White Tank Top", 366 | "Black Tank Top", 367 | "Shirtless", 368 | "Naked" 369 | ]; 370 | 371 | string[] private vehicle = [ 372 | "Dodge", 373 | "Porsche", 374 | "Tricycle", 375 | "Scooter", 376 | "ATV", 377 | "Push Bike", 378 | "Electric Scooter", 379 | "Golf Cart", 380 | "Chopper", 381 | "Rollerblades", 382 | "Lowrider", 383 | "Camper", 384 | "Rolls Royce", 385 | "BMW M3", 386 | "Bike", 387 | "C63 AMG", 388 | "G Wagon" 389 | ]; 390 | 391 | string[] private waistArmor = [ 392 | "Gucci Belt", 393 | "Versace Belt", 394 | "Studded Belt", 395 | "Taser Holster", 396 | "Concealed Holster", 397 | "Diamond Belt", 398 | "D Ring Belt", 399 | "Suspenders", 400 | "Military Belt", 401 | "Metal Belt", 402 | "Pistol Holster", 403 | "SMG Holster", 404 | "Knife Holster", 405 | "Laces", 406 | "Sash", 407 | "Fanny Pack" 408 | ]; 409 | 410 | string[] private footArmor = [ 411 | "Black Air Force 1s", 412 | "White Forces", 413 | "Air Jordan 1 Chicagos", 414 | "Gucci Tennis 84", 415 | "Air Max 95", 416 | "Timberlands", 417 | "Reebok Classics", 418 | "Flip Flops", 419 | "Nike Cortez", 420 | "Dress Shoes", 421 | "Converse All Stars", 422 | "White Slippers", 423 | "Gucci Slides", 424 | "Alligator Dress Shoes", 425 | "Socks", 426 | "Open Toe Sandals", 427 | "Barefoot" 428 | ]; 429 | 430 | string[] private handArmor = [ 431 | "Rubber Gloves", 432 | "Baseball Gloves", 433 | "Boxing Gloves", 434 | "MMA Wraps", 435 | "Winter Gloves", 436 | "Nitrile Gloves", 437 | "Studded Leather Gloves", 438 | "Combat Gloves", 439 | "Leather Gloves", 440 | "White Gloves", 441 | "Black Gloves", 442 | "Kevlar Gloves", 443 | "Surgical Gloves", 444 | "Fingerless Gloves" 445 | ]; 446 | 447 | string[] private necklaces = ["Bronze Chain", "Silver Chain", "Gold Chain"]; 448 | 449 | string[] private rings = [ 450 | "Gold Ring", 451 | "Silver Ring", 452 | "Diamond Ring", 453 | "Platinum Ring", 454 | "Titanium Ring", 455 | "Pinky Ring", 456 | "Thumb Ring" 457 | ]; 458 | 459 | string[] private suffixes = [ 460 | "from the Bayou", 461 | "from Atlanta", 462 | "from Compton", 463 | "from Oakland", 464 | "from SOMA", 465 | "from Hong Kong", 466 | "from London", 467 | "from Chicago", 468 | "from Brooklyn", 469 | "from Detroit", 470 | "from Mob Town", 471 | "from Murdertown", 472 | "from Sin City", 473 | "from Big Smoke", 474 | "from the Backwoods", 475 | "from the Big Easy", 476 | "from Queens", 477 | "from BedStuy", 478 | "from Buffalo" 479 | ]; 480 | 481 | string[] private drugs = [ 482 | "Weed", 483 | "Cocaine", 484 | "Ludes", 485 | "Acid", 486 | "Speed", 487 | "Heroin", 488 | "Oxycontin", 489 | "Zoloft", 490 | "Fentanyl", 491 | "Krokodil", 492 | "Coke", 493 | "Crack", 494 | "PCP", 495 | "LSD", 496 | "Shrooms", 497 | "Soma", 498 | "Xanax", 499 | "Molly", 500 | "Adderall" 501 | ]; 502 | 503 | string[] private namePrefixes = [ 504 | "OG", 505 | "King of the Street", 506 | "Cop Killer", 507 | "Blasta", 508 | "Lil", 509 | "Big", 510 | "Tiny", 511 | "Playboi", 512 | "Snitch boi", 513 | "Kingpin", 514 | "Father of the Game", 515 | "Son of the Game", 516 | "Loose Trigger Finger", 517 | "Slum Prince", 518 | "Corpse", 519 | "Mother of the Game", 520 | "Daughter of the Game", 521 | "Slum Princess", 522 | "Da", 523 | "Notorious", 524 | "The Boss of Bosses", 525 | "The Dog Killer", 526 | "The Killer of Dog Killer", 527 | "Slum God", 528 | "Candyman", 529 | "Candywoman", 530 | "The Butcher", 531 | "Yung Capone", 532 | "Yung Chapo", 533 | "Yung Blanco", 534 | "The Fixer", 535 | "Jail Bird", 536 | "Corner Cockatoo", 537 | "Powder Prince", 538 | "Hippie", 539 | "John E. Dell", 540 | "The Burning Man", 541 | "The Burning Woman", 542 | "Kid of the Game", 543 | "Street Queen", 544 | "The Killer of Dog Killers Killer", 545 | "Slum General", 546 | "Mafia Prince", 547 | "Crooked Cop", 548 | "Street Mayor", 549 | "Undercover Cop", 550 | "Oregano Farmer", 551 | "Bloody", 552 | "High on the Supply", 553 | "The Orphan", 554 | "The Orphan Maker", 555 | "Ex Boxer", 556 | "Ex Cop", 557 | "Ex School Teacher", 558 | "Ex Priest", 559 | "Ex Engineer", 560 | "Street Robinhood", 561 | "Hell Bound", 562 | "SoundCloud Rapper", 563 | "Gang Leader", 564 | "The CEO", 565 | "The Freelance Pharmacist", 566 | "Soccer Mom", 567 | "Soccer Dad" 568 | ]; 569 | 570 | string[] private nameSuffixes = [ 571 | "Feared", 572 | "Baron", 573 | "Vicious", 574 | "Killer", 575 | "Fugitive", 576 | "Triggerman", 577 | "Conman", 578 | "Outlaw", 579 | "Assassin", 580 | "Shooter", 581 | "Hitman", 582 | "Bloodstained", 583 | "Punishment", 584 | "Sin", 585 | "Smuggled", 586 | "LastResort", 587 | "Contraband", 588 | "Illicit" 589 | ]; 590 | 591 | function random(string memory input) internal pure returns (uint256) { 592 | return uint256(keccak256(abi.encodePacked(input))); 593 | } 594 | 595 | function getWeapon(uint256 tokenId) public view returns (string memory) { 596 | return pluck(tokenId, "WEAPON", weapons); 597 | } 598 | 599 | function getClothes(uint256 tokenId) public view returns (string memory) { 600 | return pluck(tokenId, "CLOTHES", clothes); 601 | } 602 | 603 | function getVehicle(uint256 tokenId) public view returns (string memory) { 604 | return pluck(tokenId, "VEHICLE", vehicle); 605 | } 606 | 607 | function getWaist(uint256 tokenId) public view returns (string memory) { 608 | return pluck(tokenId, "WAIST", waistArmor); 609 | } 610 | 611 | function getFoot(uint256 tokenId) public view returns (string memory) { 612 | return pluck(tokenId, "FOOT", footArmor); 613 | } 614 | 615 | function getHand(uint256 tokenId) public view returns (string memory) { 616 | return pluck(tokenId, "HAND", handArmor); 617 | } 618 | 619 | function getDrugs(uint256 tokenId) public view returns (string memory) { 620 | return pluck(tokenId, "DRUGS", drugs); 621 | } 622 | 623 | function getNeck(uint256 tokenId) public view returns (string memory) { 624 | return pluck(tokenId, "NECK", necklaces); 625 | } 626 | 627 | function getRing(uint256 tokenId) public view returns (string memory) { 628 | return pluck(tokenId, "RING", rings); 629 | } 630 | 631 | function pluck( 632 | uint256 tokenId, 633 | string memory keyPrefix, 634 | string[] memory sourceArray 635 | ) internal view returns (string memory) { 636 | uint256 rand = random( 637 | string(abi.encodePacked(keyPrefix, toString(tokenId))) 638 | ); 639 | string memory output = sourceArray[rand % sourceArray.length]; 640 | uint256 greatness = rand % 21; 641 | if (greatness > 14) { 642 | output = string( 643 | abi.encodePacked(output, " ", suffixes[rand % suffixes.length]) 644 | ); 645 | } 646 | if (greatness >= 19) { 647 | string[2] memory name; 648 | name[0] = namePrefixes[rand % namePrefixes.length]; 649 | name[1] = nameSuffixes[rand % nameSuffixes.length]; 650 | if (greatness == 19) { 651 | output = string( 652 | abi.encodePacked('"', name[0], " ", name[1], '" ', output) 653 | ); 654 | } else { 655 | output = string( 656 | abi.encodePacked( 657 | '"', 658 | name[0], 659 | " ", 660 | name[1], 661 | '" ', 662 | output, 663 | " +1" 664 | ) 665 | ); 666 | } 667 | } 668 | return output; 669 | } 670 | 671 | function tokenURI(uint256 tokenId) 672 | public 673 | view 674 | override 675 | returns (string memory) 676 | { 677 | string[17] memory parts; 678 | parts[ 679 | 0 680 | ] = ''; 681 | 682 | parts[1] = getWeapon(tokenId); 683 | 684 | parts[2] = ''; 685 | 686 | parts[3] = getClothes(tokenId); 687 | 688 | parts[4] = ''; 689 | 690 | parts[5] = getVehicle(tokenId); 691 | 692 | parts[6] = ''; 693 | 694 | parts[7] = getDrugs(tokenId); 695 | 696 | parts[8] = ''; 697 | 698 | parts[9] = getFoot(tokenId); 699 | 700 | parts[10] = ''; 701 | 702 | parts[11] = getHand(tokenId); 703 | 704 | parts[12] = ''; 705 | 706 | parts[13] = getWaist(tokenId); 707 | 708 | parts[12] = ''; 709 | 710 | parts[13] = getNeck(tokenId); 711 | 712 | parts[14] = ''; 713 | 714 | parts[15] = getRing(tokenId); 715 | 716 | parts[16] = ""; 717 | 718 | string memory output = string( 719 | abi.encodePacked( 720 | parts[0], 721 | parts[1], 722 | parts[2], 723 | parts[3], 724 | parts[4], 725 | parts[5], 726 | parts[6], 727 | parts[7], 728 | parts[8] 729 | ) 730 | ); 731 | output = string( 732 | abi.encodePacked( 733 | output, 734 | parts[9], 735 | parts[10], 736 | parts[11], 737 | parts[12], 738 | parts[13], 739 | parts[14], 740 | parts[15], 741 | parts[16] 742 | ) 743 | ); 744 | 745 | string memory json = Base64.encode( 746 | bytes( 747 | string( 748 | abi.encodePacked( 749 | '{"name": "Gear #', 750 | toString(tokenId), 751 | '", "description": "DWL is randomized street gear generated and stored on chain. Stats, images, and other functionality are intentionally omitted for others to interpret. Feel free to use Loot in any way you want.", "image": "data:image/svg+xml;base64,', 752 | Base64.encode(bytes(output)), 753 | '"}' 754 | ) 755 | ) 756 | ) 757 | ); 758 | output = string( 759 | abi.encodePacked("data:application/json;base64,", json) 760 | ); 761 | 762 | return output; 763 | } 764 | 765 | function claim(uint256 tokenId) public nonReentrant { 766 | //CHANGE: limit 767 | require(tokenId > 0 && tokenId < 8001, "Token ID invalid"); 768 | _safeMint(_msgSender(), tokenId); 769 | } 770 | 771 | function toString(uint256 value) internal pure returns (string memory) { 772 | // Inspired by OraclizeAPI's implementation - MIT license 773 | // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol 774 | 775 | if (value == 0) { 776 | return "0"; 777 | } 778 | uint256 temp = value; 779 | uint256 digits; 780 | while (temp != 0) { 781 | digits++; 782 | temp /= 10; 783 | } 784 | bytes memory buffer = new bytes(digits); 785 | while (value != 0) { 786 | digits -= 1; 787 | buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); 788 | value /= 10; 789 | } 790 | return string(buffer); 791 | } 792 | 793 | constructor() ERC721("DOPE", "DOPE") Ownable() {} 794 | } -------------------------------------------------------------------------------- /contracts/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // contracts/GLDToken.sol 2 | // SPDX-License-Identifier: MIT 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | 7 | contract MockERC20 is ERC20 { 8 | constructor(uint256 initialSupply) ERC20("Mock ERC20", "MERC") { 9 | _mint(msg.sender, initialSupply); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/MockERC721.sol: -------------------------------------------------------------------------------- 1 | // contracts/GameItem.sol 2 | // SPDX-License-Identifier: MIT 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; 6 | import "@openzeppelin/contracts/utils/Counters.sol"; 7 | 8 | contract MockERC721 is ERC721URIStorage { 9 | using Counters for Counters.Counter; 10 | Counters.Counter private _tokenIds; 11 | 12 | constructor() ERC721("MockNFT", "MNFT") {} 13 | 14 | function mint(address recipient, string memory tokenURI) public returns (uint256) { 15 | _tokenIds.increment(); 16 | 17 | uint256 newItemId = _tokenIds.current(); 18 | _mint(recipient, newItemId); 19 | _setTokenURI(newItemId, tokenURI); 20 | 21 | return newItemId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/NftStake.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 7 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 9 | 10 | contract NftStake is IERC721Receiver, ReentrancyGuard { 11 | using SafeMath for uint256; 12 | 13 | IERC721 public nftToken; 14 | IERC20 public erc20Token; 15 | 16 | string public constant TERMS_OF_SERVICE = 17 | 'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'; 18 | 19 | address public admin; 20 | uint256 public emissionRate; 21 | 22 | struct Stake { 23 | uint256 from; 24 | address owner; 25 | } 26 | 27 | // TokenID => Stake 28 | mapping(uint256 => Stake) public receipt; 29 | 30 | event Staked(address indexed staker, uint256 tokenId, uint256 block); 31 | event Unstaked(address indexed staker, uint256 tokenId, uint256 block); 32 | event Payout(address indexed staker, uint256 tokenId, uint256 amount, uint256 fromBlock, uint256 toBlock); 33 | event EmissionRateUpdate(uint256 rate); 34 | 35 | modifier onlyStaker(uint256 tokenId) { 36 | // require that this contract has the NFT 37 | require(nftToken.ownerOf(tokenId) == address(this), "nftstake: not owned"); 38 | 39 | // require that this token is staked 40 | require(receipt[tokenId].from != 0, "nftstake: not staked"); 41 | 42 | // require that msg.sender is the owner of this nft 43 | require(receipt[tokenId].owner == msg.sender, "nftstake: not owner"); 44 | 45 | _; 46 | } 47 | 48 | modifier onlyAdmin() { 49 | require(msg.sender == admin, "nftstake: not admin"); 50 | _; 51 | } 52 | 53 | modifier acceptedTermsOfService(bool accepted) { 54 | require(accepted, "nftstake: must accept terms of service"); 55 | _; 56 | } 57 | 58 | constructor( 59 | IERC721 _nftToken, 60 | IERC20 _erc20Token, 61 | address _admin, 62 | uint256 _emissionRate 63 | ) { 64 | nftToken = _nftToken; 65 | erc20Token = _erc20Token; 66 | admin = _admin; 67 | emissionRate = _emissionRate; 68 | 69 | emit EmissionRateUpdate(emissionRate); 70 | } 71 | 72 | // User must give this contract permission to take ownership of it. 73 | function stake(uint256[] calldata ids, bool iAcceptTermOfService) 74 | public 75 | nonReentrant 76 | acceptedTermsOfService(iAcceptTermOfService) 77 | returns (bool) 78 | { 79 | for (uint256 i = 0; i < ids.length; i++) { 80 | _stake(ids[i]); 81 | } 82 | return true; 83 | } 84 | 85 | function unstake(uint256[] calldata ids, bool iAcceptTermOfService) 86 | public 87 | nonReentrant 88 | acceptedTermsOfService(iAcceptTermOfService) 89 | returns (bool) 90 | { 91 | for (uint256 i = 0; i < ids.length; i++) { 92 | _unstake(ids[i]); 93 | } 94 | return true; 95 | } 96 | 97 | function harvest(uint256[] calldata ids, bool iAcceptTermOfService) 98 | public 99 | nonReentrant 100 | acceptedTermsOfService(iAcceptTermOfService) 101 | { 102 | for (uint256 i = 0; i < ids.length; i++) { 103 | _harvest(ids[i]); 104 | } 105 | } 106 | 107 | function sweep() external onlyAdmin { 108 | erc20Token.transfer(admin, erc20Token.balanceOf(address(this))); 109 | } 110 | 111 | function _stake(uint256 tokenId) internal returns (bool) { 112 | receipt[tokenId].from = block.number; 113 | receipt[tokenId].owner = msg.sender; 114 | nftToken.safeTransferFrom(msg.sender, address(this), tokenId); 115 | emit Staked(msg.sender, tokenId, block.number); 116 | return true; 117 | } 118 | 119 | function _unstake(uint256 tokenId) internal onlyStaker(tokenId) returns (bool) { 120 | if (receipt[tokenId].from < block.number) { 121 | // payout stake, this should be safe as the function is non-reentrant 122 | _payout(tokenId); 123 | } 124 | 125 | delete receipt[tokenId]; 126 | nftToken.safeTransferFrom(address(this), msg.sender, tokenId); 127 | emit Unstaked(msg.sender, tokenId, block.number); 128 | return true; 129 | } 130 | 131 | function _harvest(uint256 tokenId) internal onlyStaker(tokenId) { 132 | require(receipt[tokenId].from < block.number, "nftstake: too soon"); 133 | 134 | // payout stake, this should be safe as the function is non-reentrant 135 | _payout(tokenId); 136 | receipt[tokenId].from = block.number; 137 | } 138 | 139 | function _payout(uint256 tokenId) internal { 140 | /* NOTE : Must be called from non-reentrant function to be safe!*/ 141 | require(receipt[tokenId].from != 0, "nftstake: not staked"); 142 | 143 | // earned amount is difference between the stake start block, current block multiplied by stake amount 144 | uint256 duration = block.number.sub(receipt[tokenId].from).sub(1); // don't pay for the tx block of withdrawl 145 | uint256 reward = duration.mul(emissionRate); 146 | 147 | // If contract does not have enough tokens to pay out, return the NFT without payment 148 | // This prevent a NFT being locked in the contract when empty 149 | if (erc20Token.balanceOf(address(this)) < reward) { 150 | emit Payout(msg.sender, tokenId, 0, receipt[tokenId].from, block.number); 151 | return; 152 | } 153 | 154 | erc20Token.transfer(receipt[tokenId].owner, reward); 155 | 156 | emit Payout(msg.sender, tokenId, reward, receipt[tokenId].from, block.number); 157 | } 158 | 159 | function rewardOf(uint256 tokenId) public view returns (uint256) { 160 | if (receipt[tokenId].from == 0) { 161 | return 0; 162 | } 163 | 164 | return block.number.sub(receipt[tokenId].from).mul(emissionRate); 165 | } 166 | 167 | function setEmissionRate(uint256 _emissionRate) external onlyAdmin { 168 | emissionRate = _emissionRate; 169 | emit EmissionRateUpdate(emissionRate); 170 | } 171 | 172 | /** 173 | * Always returns `IERC721Receiver.onERC721Received.selector`. 174 | */ 175 | function onERC721Received( 176 | address, 177 | address, 178 | uint256, 179 | bytes memory 180 | ) public virtual override returns (bytes4) { 181 | return this.onERC721Received.selector; 182 | } 183 | 184 | /** Add Function to allow the DAO to forcibly unstake an NFT and return it to the owner */ 185 | } 186 | -------------------------------------------------------------------------------- /contracts/Paper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Snapshot.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | 11 | contract Paper is ERC20, ERC20Permit, ERC20Votes, ERC20Snapshot, Ownable { 12 | // Dope Wars Loot: https://etherscan.io/address/0x8707276DF042E89669d69A177d3DA7dC78bd8723 13 | IERC721Enumerable public loot = IERC721Enumerable(0x8707276DF042E89669d69A177d3DA7dC78bd8723); 14 | // DopeDAO timelock: https://etherscan.io/address/0xb57ab8767cae33be61ff15167134861865f7d22c 15 | address public timelock = 0xB57Ab8767CAe33bE61fF15167134861865F7D22C; 16 | 17 | // 8000 tokens number 1-8000 18 | uint256 public tokenIdStart = 1; 19 | uint256 public tokenIdEnd = 8000; 20 | 21 | // Give out 1bn of tokens, evenly split across each NFT 22 | uint256 public paperPerTokenId = (1000000000 * (10**decimals())) / tokenIdEnd; 23 | 24 | // track claimedTokens 25 | mapping(uint256 => bool) public claimedByTokenId; 26 | 27 | constructor() ERC20("Paper", "PAPER") ERC20Permit("PAPER") { 28 | transferOwnership(timelock); 29 | } 30 | 31 | function snapshot() external onlyOwner { 32 | _snapshot(); 33 | } 34 | 35 | /// @notice Claim Paper for a given Dope Wars Loot ID 36 | /// @param tokenId The tokenId of the Dope Wars Loot NFT 37 | function claimById(uint256 tokenId) external { 38 | // Follow the Checks-Effects-Interactions pattern to prevent reentrancy 39 | // attacks 40 | 41 | // Checks 42 | 43 | // Check that the msgSender owns the token that is being claimed 44 | require(_msgSender() == loot.ownerOf(tokenId), "MUST_OWN_TOKEN_ID"); 45 | 46 | // Further Checks, Effects, and Interactions are contained within the 47 | // _claim() function 48 | _claim(tokenId, _msgSender()); 49 | } 50 | 51 | /// @notice Claim Paper for all tokens owned by the sender 52 | /// @notice This function will run out of gas if you have too much loot! If 53 | /// this is a concern, you should use claimRangeForOwner and claim Dope in 54 | /// batches. 55 | function claimAllForOwner() external { 56 | uint256 tokenBalanceOwner = loot.balanceOf(_msgSender()); 57 | 58 | // Checks 59 | require(tokenBalanceOwner > 0, "NO_TOKENS_OWNED"); 60 | 61 | // i < tokenBalanceOwner because tokenBalanceOwner is 1-indexed 62 | for (uint256 i = 0; i < tokenBalanceOwner; i++) { 63 | // Further Checks, Effects, and Interactions are contained within 64 | // the _claim() function 65 | _claim(loot.tokenOfOwnerByIndex(_msgSender(), i), _msgSender()); 66 | } 67 | } 68 | 69 | /// @notice Claim Paper for all tokens owned by the sender within a 70 | /// given range 71 | /// @notice This function is useful if you own too much DWL to claim all at 72 | /// once or if you want to leave some Paper unclaimed. 73 | function claimRangeForOwner(uint256 ownerIndexStart, uint256 ownerIndexEnd) external { 74 | uint256 tokenBalanceOwner = loot.balanceOf(_msgSender()); 75 | 76 | // Checks 77 | require(tokenBalanceOwner > 0, "NO_TOKENS_OWNED"); 78 | 79 | // We use < for ownerIndexEnd and tokenBalanceOwner because 80 | // tokenOfOwnerByIndex is 0-indexed while the token balance is 1-indexed 81 | require(ownerIndexStart >= 0 && ownerIndexEnd < tokenBalanceOwner, "INDEX_OUT_OF_RANGE"); 82 | 83 | // i <= ownerIndexEnd because ownerIndexEnd is 0-indexed 84 | for (uint256 i = ownerIndexStart; i <= ownerIndexEnd; i++) { 85 | // Further Checks, Effects, and Interactions are contained within 86 | // the _claim() function 87 | _claim(loot.tokenOfOwnerByIndex(_msgSender(), i), _msgSender()); 88 | } 89 | } 90 | 91 | /// @dev Internal function to mint Paper upon claiming 92 | function _claim(uint256 tokenId, address tokenOwner) internal { 93 | // Checks 94 | // Check that the token ID is in range 95 | // We use >= and <= to here because all of the token IDs are 0-indexed 96 | require(tokenId >= tokenIdStart && tokenId <= tokenIdEnd, "TOKEN_ID_OUT_OF_RANGE"); 97 | 98 | // Check that Paper have not already been claimed for a given tokenId 99 | require(!claimedByTokenId[tokenId], "PAPER_CLAIMED_FOR_TOKEN_ID"); 100 | 101 | // Effects 102 | 103 | // Mark that Paper has been claimed for the 104 | // given tokenId 105 | claimedByTokenId[tokenId] = true; 106 | 107 | // Interactions 108 | 109 | // Send Paper to the owner of the token ID 110 | _mint(tokenOwner, paperPerTokenId); 111 | } 112 | 113 | function mint(address to, uint256 amount) external onlyOwner { 114 | _mint(to, amount); 115 | } 116 | 117 | // The following functions are overrides required by Solidity. 118 | 119 | function _beforeTokenTransfer( 120 | address from, 121 | address to, 122 | uint256 amount 123 | ) internal override(ERC20, ERC20Snapshot) { 124 | super._beforeTokenTransfer(from, to, amount); 125 | } 126 | 127 | function _afterTokenTransfer( 128 | address from, 129 | address to, 130 | uint256 amount 131 | ) internal override(ERC20, ERC20Votes) { 132 | super._afterTokenTransfer(from, to, amount); 133 | } 134 | 135 | function _mint(address to, uint256 amount) internal override(ERC20, ERC20Votes) { 136 | super._mint(to, amount); 137 | } 138 | 139 | function _burn(address account, uint256 amount) internal override(ERC20, ERC20Votes) { 140 | super._burn(account, amount); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /contracts/governance/DopeDAO.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/governance/Governor.sol"; 5 | import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; 6 | import "@openzeppelin/contracts/governance/extensions/GovernorVotesComp.sol"; 7 | import "@openzeppelin/contracts/governance/extensions/GovernorTimelockCompound.sol"; 8 | 9 | contract DopeDAO is Governor, GovernorCompatibilityBravo, GovernorVotesComp, GovernorTimelockCompound { 10 | constructor(ERC20VotesComp _token, ICompoundTimelock _timelock) 11 | Governor("DopeDAO") 12 | GovernorVotesComp(_token) 13 | GovernorTimelockCompound(_timelock) 14 | {} 15 | 16 | function votingDelay() public pure virtual override returns (uint256) { 17 | return 13091; // 2 days (in blocks) 18 | } 19 | 20 | function votingPeriod() public pure virtual override returns (uint256) { 21 | return 45818; // 1 week (in blocks) 22 | } 23 | 24 | function quorum(uint256 blockNumber) public pure virtual override returns (uint256) { 25 | return 500; // DOPE DAO NFT TOKENS 26 | } 27 | 28 | function proposalThreshold() public pure virtual override returns (uint256) { 29 | return 50; // DOPE DAO NFT TOKENS 30 | } 31 | 32 | // The following functions are overrides required by Solidity. 33 | 34 | function getVotes(address account, uint256 blockNumber) 35 | public 36 | view 37 | override(IGovernor, GovernorVotesComp) 38 | returns (uint256) 39 | { 40 | return super.getVotes(account, blockNumber); 41 | } 42 | 43 | function state(uint256 proposalId) 44 | public 45 | view 46 | override(Governor, IGovernor, GovernorTimelockCompound) 47 | returns (ProposalState) 48 | { 49 | return super.state(proposalId); 50 | } 51 | 52 | function propose( 53 | address[] memory targets, 54 | uint256[] memory values, 55 | bytes[] memory calldatas, 56 | string memory description 57 | ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { 58 | return super.propose(targets, values, calldatas, description); 59 | } 60 | 61 | function _execute( 62 | uint256 proposalId, 63 | address[] memory targets, 64 | uint256[] memory values, 65 | bytes[] memory calldatas, 66 | bytes32 /*descriptionHash*/ 67 | ) internal override(Governor, GovernorTimelockCompound) { 68 | uint256 eta = proposalEta(proposalId); 69 | require(eta > 0, "GovernorTimelockCompound: proposal not yet queued"); 70 | Address.sendValue(payable(timelock()), msg.value); 71 | for (uint256 i = 0; i < targets.length; ++i) { 72 | ICompoundTimelock(payable(timelock())).executeTransaction(targets[i], values[i], "", calldatas[i], eta); 73 | } 74 | } 75 | 76 | function _cancel( 77 | address[] memory targets, 78 | uint256[] memory values, 79 | bytes[] memory calldatas, 80 | bytes32 descriptionHash 81 | ) internal override(Governor, GovernorTimelockCompound) returns (uint256) { 82 | return super._cancel(targets, values, calldatas, descriptionHash); 83 | } 84 | 85 | function _executor() internal view override(Governor, GovernorTimelockCompound) returns (address) { 86 | return super._executor(); 87 | } 88 | 89 | function supportsInterface(bytes4 interfaceId) 90 | public 91 | view 92 | override(Governor, IERC165, GovernorTimelockCompound) 93 | returns (bool) 94 | { 95 | return super.supportsInterface(interfaceId); 96 | } 97 | 98 | /** 99 | * @dev Function to receive ETH that will be handled by the governor (disabled if executor is a third party contract) 100 | */ 101 | receive() external payable virtual { 102 | require(_executor() == address(this)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /contracts/governance/Timelock.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2021-09-01 3 | */ 4 | 5 | // Sources flattened with hardhat v2.6.1 https://hardhat.org 6 | 7 | // File contracts/SafeMath.sol 8 | 9 | pragma solidity ^0.5.16; 10 | 11 | // From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol 12 | // Subject to the MIT license. 13 | 14 | /** 15 | * @dev Wrappers over Solidity's arithmetic operations with added overflow 16 | * checks. 17 | * 18 | * Arithmetic operations in Solidity wrap on overflow. This can easily result 19 | * in bugs, because programmers usually assume that an overflow raises an 20 | * error, which is the standard behavior in high level programming languages. 21 | * `SafeMath` restores this intuition by reverting the transaction when an 22 | * operation overflows. 23 | * 24 | * Using this library instead of the unchecked operations eliminates an entire 25 | * class of bugs, so it's recommended to use it always. 26 | */ 27 | library SafeMath { 28 | /** 29 | * @dev Returns the addition of two unsigned integers, reverting on overflow. 30 | * 31 | * Counterpart to Solidity's `+` operator. 32 | * 33 | * Requirements: 34 | * - Addition cannot overflow. 35 | */ 36 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 37 | uint256 c = a + b; 38 | require(c >= a, "SafeMath: addition overflow"); 39 | 40 | return c; 41 | } 42 | 43 | /** 44 | * @dev Returns the addition of two unsigned integers, reverting with custom message on overflow. 45 | * 46 | * Counterpart to Solidity's `+` operator. 47 | * 48 | * Requirements: 49 | * - Addition cannot overflow. 50 | */ 51 | function add( 52 | uint256 a, 53 | uint256 b, 54 | string memory errorMessage 55 | ) internal pure returns (uint256) { 56 | uint256 c = a + b; 57 | require(c >= a, errorMessage); 58 | 59 | return c; 60 | } 61 | 62 | /** 63 | * @dev Returns the subtraction of two unsigned integers, reverting on underflow (when the result is negative). 64 | * 65 | * Counterpart to Solidity's `-` operator. 66 | * 67 | * Requirements: 68 | * - Subtraction cannot underflow. 69 | */ 70 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 71 | return sub(a, b, "SafeMath: subtraction underflow"); 72 | } 73 | 74 | /** 75 | * @dev Returns the subtraction of two unsigned integers, reverting with custom message on underflow (when the result is negative). 76 | * 77 | * Counterpart to Solidity's `-` operator. 78 | * 79 | * Requirements: 80 | * - Subtraction cannot underflow. 81 | */ 82 | function sub( 83 | uint256 a, 84 | uint256 b, 85 | string memory errorMessage 86 | ) internal pure returns (uint256) { 87 | require(b <= a, errorMessage); 88 | uint256 c = a - b; 89 | 90 | return c; 91 | } 92 | 93 | /** 94 | * @dev Returns the multiplication of two unsigned integers, reverting on overflow. 95 | * 96 | * Counterpart to Solidity's `*` operator. 97 | * 98 | * Requirements: 99 | * - Multiplication cannot overflow. 100 | */ 101 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 102 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 103 | // benefit is lost if 'b' is also tested. 104 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 105 | if (a == 0) { 106 | return 0; 107 | } 108 | 109 | uint256 c = a * b; 110 | require(c / a == b, "SafeMath: multiplication overflow"); 111 | 112 | return c; 113 | } 114 | 115 | /** 116 | * @dev Returns the multiplication of two unsigned integers, reverting on overflow. 117 | * 118 | * Counterpart to Solidity's `*` operator. 119 | * 120 | * Requirements: 121 | * - Multiplication cannot overflow. 122 | */ 123 | function mul( 124 | uint256 a, 125 | uint256 b, 126 | string memory errorMessage 127 | ) internal pure returns (uint256) { 128 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 129 | // benefit is lost if 'b' is also tested. 130 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 131 | if (a == 0) { 132 | return 0; 133 | } 134 | 135 | uint256 c = a * b; 136 | require(c / a == b, errorMessage); 137 | 138 | return c; 139 | } 140 | 141 | /** 142 | * @dev Returns the integer division of two unsigned integers. 143 | * Reverts on division by zero. The result is rounded towards zero. 144 | * 145 | * Counterpart to Solidity's `/` operator. Note: this function uses a 146 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 147 | * uses an invalid opcode to revert (consuming all remaining gas). 148 | * 149 | * Requirements: 150 | * - The divisor cannot be zero. 151 | */ 152 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 153 | return div(a, b, "SafeMath: division by zero"); 154 | } 155 | 156 | /** 157 | * @dev Returns the integer division of two unsigned integers. 158 | * Reverts with custom message on division by zero. The result is rounded towards zero. 159 | * 160 | * Counterpart to Solidity's `/` operator. Note: this function uses a 161 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 162 | * uses an invalid opcode to revert (consuming all remaining gas). 163 | * 164 | * Requirements: 165 | * - The divisor cannot be zero. 166 | */ 167 | function div( 168 | uint256 a, 169 | uint256 b, 170 | string memory errorMessage 171 | ) internal pure returns (uint256) { 172 | // Solidity only automatically asserts when dividing by 0 173 | require(b > 0, errorMessage); 174 | uint256 c = a / b; 175 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 176 | 177 | return c; 178 | } 179 | 180 | /** 181 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 182 | * Reverts when dividing by zero. 183 | * 184 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 185 | * opcode (which leaves remaining gas untouched) while Solidity uses an 186 | * invalid opcode to revert (consuming all remaining gas). 187 | * 188 | * Requirements: 189 | * - The divisor cannot be zero. 190 | */ 191 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 192 | return mod(a, b, "SafeMath: modulo by zero"); 193 | } 194 | 195 | /** 196 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 197 | * Reverts with custom message when dividing by zero. 198 | * 199 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 200 | * opcode (which leaves remaining gas untouched) while Solidity uses an 201 | * invalid opcode to revert (consuming all remaining gas). 202 | * 203 | * Requirements: 204 | * - The divisor cannot be zero. 205 | */ 206 | function mod( 207 | uint256 a, 208 | uint256 b, 209 | string memory errorMessage 210 | ) internal pure returns (uint256) { 211 | require(b != 0, errorMessage); 212 | return a % b; 213 | } 214 | } 215 | 216 | // File contracts/Timelock.sol 217 | 218 | pragma solidity ^0.5.16; 219 | 220 | contract Timelock { 221 | using SafeMath for uint256; 222 | 223 | event NewAdmin(address indexed newAdmin); 224 | event NewPendingAdmin(address indexed newPendingAdmin); 225 | event NewDelay(uint256 indexed newDelay); 226 | event CancelTransaction( 227 | bytes32 indexed txHash, 228 | address indexed target, 229 | uint256 value, 230 | string signature, 231 | bytes data, 232 | uint256 eta 233 | ); 234 | event ExecuteTransaction( 235 | bytes32 indexed txHash, 236 | address indexed target, 237 | uint256 value, 238 | string signature, 239 | bytes data, 240 | uint256 eta 241 | ); 242 | event QueueTransaction( 243 | bytes32 indexed txHash, 244 | address indexed target, 245 | uint256 value, 246 | string signature, 247 | bytes data, 248 | uint256 eta 249 | ); 250 | 251 | uint256 public constant GRACE_PERIOD = 14 days; 252 | uint256 public constant MINIMUM_DELAY = 1 seconds; 253 | uint256 public constant MAXIMUM_DELAY = 30 days; 254 | 255 | address public admin; 256 | address public pendingAdmin; 257 | uint256 public delay; 258 | 259 | mapping(bytes32 => bool) public queuedTransactions; 260 | 261 | constructor(address admin_, uint256 delay_) public { 262 | require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); 263 | require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); 264 | 265 | admin = admin_; 266 | delay = delay_; 267 | } 268 | 269 | function() external payable {} 270 | 271 | function setDelay(uint256 delay_) public { 272 | require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); 273 | require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); 274 | require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); 275 | delay = delay_; 276 | 277 | emit NewDelay(delay); 278 | } 279 | 280 | function acceptAdmin() public { 281 | require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); 282 | admin = msg.sender; 283 | pendingAdmin = address(0); 284 | 285 | emit NewAdmin(admin); 286 | } 287 | 288 | function setPendingAdmin(address pendingAdmin_) public { 289 | require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); 290 | pendingAdmin = pendingAdmin_; 291 | 292 | emit NewPendingAdmin(pendingAdmin); 293 | } 294 | 295 | function queueTransaction( 296 | address target, 297 | uint256 value, 298 | string memory signature, 299 | bytes memory data, 300 | uint256 eta 301 | ) public returns (bytes32) { 302 | require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); 303 | require( 304 | eta >= getBlockTimestamp().add(delay), 305 | "Timelock::queueTransaction: Estimated execution block must satisfy delay." 306 | ); 307 | 308 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 309 | queuedTransactions[txHash] = true; 310 | 311 | emit QueueTransaction(txHash, target, value, signature, data, eta); 312 | return txHash; 313 | } 314 | 315 | function cancelTransaction( 316 | address target, 317 | uint256 value, 318 | string memory signature, 319 | bytes memory data, 320 | uint256 eta 321 | ) public { 322 | require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); 323 | 324 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 325 | queuedTransactions[txHash] = false; 326 | 327 | emit CancelTransaction(txHash, target, value, signature, data, eta); 328 | } 329 | 330 | function executeTransaction( 331 | address target, 332 | uint256 value, 333 | string memory signature, 334 | bytes memory data, 335 | uint256 eta 336 | ) public payable returns (bytes memory) { 337 | require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); 338 | 339 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 340 | require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); 341 | require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); 342 | require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); 343 | 344 | queuedTransactions[txHash] = false; 345 | 346 | bytes memory callData; 347 | 348 | if (bytes(signature).length == 0) { 349 | callData = data; 350 | } else { 351 | callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); 352 | } 353 | 354 | // solium-disable-next-line security/no-call-value 355 | (bool success, bytes memory returnData) = target.call.value(value)(callData); 356 | require(success, "Timelock::executeTransaction: Transaction execution reverted."); 357 | 358 | emit ExecuteTransaction(txHash, target, value, signature, data, eta); 359 | 360 | return returnData; 361 | } 362 | 363 | function getBlockTimestamp() internal view returns (uint256) { 364 | // solium-disable-next-line security/no-block-members 365 | return block.timestamp; 366 | } 367 | } 368 | 369 | // File contracts/Mock.sol 370 | 371 | pragma solidity ^0.5.16; 372 | 373 | contract Mock { 374 | event Received(string message, uint256 ethAmount); 375 | 376 | string public message; 377 | 378 | function receiveETH(string memory _message) public payable { 379 | message = _message; 380 | emit Received(_message, msg.value); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /contracts/test/DopeDAO.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.2; 3 | 4 | import "@openzeppelin/contracts/governance/Governor.sol"; 5 | import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; 6 | import "@openzeppelin/contracts/governance/extensions/GovernorVotesComp.sol"; 7 | import "@openzeppelin/contracts/governance/extensions/GovernorTimelockCompound.sol"; 8 | 9 | import "../governance/DopeDAO.sol"; 10 | 11 | contract DopeDAOTest is DopeDAO { 12 | constructor(ERC20VotesComp _token, ICompoundTimelock _timelock) DopeDAO(_token, _timelock) {} 13 | 14 | function votingDelay() public pure override returns (uint256) { 15 | return 1; 16 | } 17 | 18 | function votingPeriod() public pure override returns (uint256) { 19 | return 2; 20 | } 21 | 22 | function quorum(uint256 blockNumber) public pure override returns (uint256) { 23 | return 1; 24 | } 25 | 26 | function proposalThreshold() public pure override returns (uint256) { 27 | return 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/test/Receiver.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.2; 2 | 3 | contract Receiver { 4 | function receiveEth(string calldata) public payable {} 5 | 6 | function receiveNoEth(string calldata) public {} 7 | } 8 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { config as dotenvConfig } from "dotenv"; 2 | import { resolve } from "path"; 3 | dotenvConfig({ path: resolve(__dirname, "./.env") }); 4 | 5 | import { HardhatUserConfig } from "hardhat/config"; 6 | import { NetworkUserConfig } from "hardhat/types"; 7 | import "@nomiclabs/hardhat-etherscan"; 8 | import "@nomiclabs/hardhat-waffle"; 9 | import "@typechain/hardhat"; 10 | import "hardhat-gas-reporter"; 11 | import "solidity-coverage"; 12 | 13 | import { url, accounts } from './utils/network'; 14 | 15 | import "./tasks/accounts"; 16 | import "./tasks/clean"; 17 | import "./tasks/deployers"; 18 | 19 | const config: HardhatUserConfig = { 20 | defaultNetwork: "hardhat", 21 | networks: { 22 | hardhat: { 23 | chainId: 1, 24 | // process.env.HARDHAT_FORK will specify the network that the fork is made from. 25 | // this line ensure the use of the corresponding accounts 26 | accounts: accounts(process.env.HARDHAT_FORK), 27 | forking: process.env.HARDHAT_FORK 28 | ? { 29 | url: url(process.env.HARDHAT_FORK), 30 | // After DOPE, Timelock, PAPER, DAO deploys 31 | blockNumber: 13251088, 32 | } 33 | : undefined, 34 | }, 35 | localhost: { 36 | url: url('localhost'), 37 | accounts: accounts(), 38 | }, 39 | mainnet: { 40 | url: url('mainnet'), 41 | accounts: accounts('mainnet'), 42 | }, 43 | rinkeby: { 44 | url: url('rinkeby'), 45 | accounts: accounts('rinkeby'), 46 | }, 47 | kovan: { 48 | url: url('kovan'), 49 | accounts: accounts('kovan'), 50 | }, 51 | goerli: { 52 | url: url('goerli'), 53 | accounts: accounts('goerli'), 54 | }, 55 | }, 56 | paths: { 57 | artifacts: "./artifacts", 58 | cache: "./cache", 59 | sources: "./contracts", 60 | tests: "./test", 61 | }, 62 | solidity: { 63 | compilers: [{ 64 | version: "0.8.6", 65 | settings: { 66 | metadata: { 67 | // Not including the metadata hash 68 | // https://github.com/paulrberg/solidity-template/issues/31 69 | bytecodeHash: "none", 70 | }, 71 | // Disable the optimizer when debugging 72 | // https://hardhat.org/hardhat-network/#solidity-optimizer-support 73 | optimizer: { 74 | enabled: true, 75 | runs: 800, 76 | }, 77 | }, 78 | }, 79 | { 80 | version: '0.5.17', 81 | settings: { 82 | optimizer: { 83 | enabled: true, 84 | runs: 100, 85 | }, 86 | }, 87 | }], 88 | }, 89 | typechain: { 90 | outDir: "typechain", 91 | target: "ethers-v5", 92 | }, 93 | gasReporter: { 94 | currency: "USD", 95 | enabled: process.env.REPORT_GAS ? true : false, 96 | excludeContracts: [], 97 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 98 | src: "./contracts", 99 | }, 100 | etherscan: { 101 | apiKey: "RWNVM4YY577I58CZHRDUSKZJ4CVW3S31YM", 102 | }, 103 | }; 104 | 105 | export default config; 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dopedao/contracts", 3 | "description": "DopeDAO smart contracts", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@codechecks/client": "^0.1.11", 7 | "@commitlint/cli": "^13.1.0", 8 | "@commitlint/config-conventional": "^13.1.0", 9 | "@ethersproject/abi": "^5.4.0", 10 | "@ethersproject/abstract-signer": "^5.4.1", 11 | "@ethersproject/bignumber": "^5.4.1", 12 | "@ethersproject/bytes": "^5.4.0", 13 | "@ethersproject/providers": "^5.4.4", 14 | "@nomiclabs/hardhat-ethers": "^2.0.2", 15 | "@nomiclabs/hardhat-waffle": "^2.0.1", 16 | "@typechain/ethers-v5": "^7.0.1", 17 | "@typechain/hardhat": "^2.3.0", 18 | "@types/chai": "^4.2.21", 19 | "@types/fs-extra": "^9.0.12", 20 | "@types/mocha": "^9.0.0", 21 | "@types/node": "^16.7.1", 22 | "@typescript-eslint/eslint-plugin": "^4.29.2", 23 | "@typescript-eslint/parser": "^4.29.2", 24 | "chai": "^4.3.4", 25 | "commitizen": "^4.2.4", 26 | "cross-env": "^7.0.3", 27 | "cz-conventional-changelog": "^3.3.0", 28 | "dotenv": "^10.0.0", 29 | "eslint": "^7.32.0", 30 | "eslint-config-prettier": "^8.3.0", 31 | "ethereum-waffle": "^3.4.0", 32 | "ethers": "^5.4.5", 33 | "fs-extra": "^10.0.0", 34 | "hardhat": "^2.6.1", 35 | "hardhat-deploy": "^0.9.1", 36 | "hardhat-gas-reporter": "^1.0.4", 37 | "lint-staged": "^11.1.2", 38 | "mocha": "^9.1.0", 39 | "prettier": "^2.3.2", 40 | "prettier-plugin-solidity": "^1.0.0-beta.17", 41 | "shelljs": "^0.8.4", 42 | "solhint": "^3.3.6", 43 | "solhint-plugin-prettier": "^0.0.5", 44 | "solidity-coverage": "^0.7.16", 45 | "ts-generator": "^0.1.1", 46 | "ts-node": "^10.2.1", 47 | "typechain": "^5.1.2", 48 | "typescript": "^4.3.5" 49 | }, 50 | "files": [ 51 | "/contracts" 52 | ], 53 | "keywords": [ 54 | "blockchain", 55 | "ethereum", 56 | "hardhat", 57 | "smart-contracts", 58 | "solidity" 59 | ], 60 | "private": true, 61 | "resolutions": { 62 | "@solidity-parser/parser": "^0.13.2" 63 | }, 64 | "scripts": { 65 | "clean": "hardhat clean", 66 | "compile": "hardhat compile", 67 | "coverage": "cross-env CODE_COVERAGE=true hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"./test/**/*.ts\"", 68 | "deploy": "hardhat deploy:NFTStake", 69 | "lint": "yarn run lint:sol && yarn run lint:ts && yarn run prettier:check", 70 | "lint:sol": "solhint --config ./.solhint.json --max-warnings 0 \"contracts/**/*.sol\"", 71 | "lint:ts": "eslint --config ./.eslintrc.yaml --ignore-path ./.eslintignore --ext .js,.ts .", 72 | "prettier": "prettier --config ./.prettierrc.yaml --write \"**/*.{js,json,md,sol,ts}\"", 73 | "prettier:check": "prettier --check --config ./.prettierrc.yaml \"**/*.{js,json,md,sol,ts}\"", 74 | "test": "hardhat test", 75 | "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain", 76 | "fork:test": "HARDHAT_FORK=mainnet hardhat test", 77 | "fork:run": "HARDHAT_FORK=mainnet hardhat run" 78 | }, 79 | "dependencies": { 80 | "@nomiclabs/hardhat-etherscan": "^2.1.6", 81 | "@openzeppelin/contracts": "^4.3.2" 82 | } 83 | } -------------------------------------------------------------------------------- /scripts/daorun.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat" 2 | 3 | import { DopeWarsLoot__factory, DopeDAO__factory, ICompoundTimelock__factory, Receiver, Receiver__factory } from "../typechain" 4 | 5 | const LOOT = "0x8707276df042e89669d69a177d3da7dc78bd8723" 6 | const DAO = "0xDBd38F7e739709fe5bFaE6cc8eF67C3820830E0C" 7 | const TIMELOCK = "0xb57ab8767cae33be61ff15167134861865f7d22c" 8 | 9 | const MS = "0xB429Bee46B7DF01D759D04D57DaBe814ECf0341b" 10 | 11 | async function main() { 12 | const signers = await ethers.getSigners() 13 | 14 | const receiverFactory: Receiver__factory = await ethers.getContractFactory("Receiver"); 15 | const receiver: Receiver = (await receiverFactory.deploy()); 16 | await receiver.deployed(); 17 | 18 | console.log("Receiver at ", receiver.address) 19 | 20 | const loot = DopeWarsLoot__factory.connect(LOOT, signers[0]) 21 | const dao = DopeDAO__factory.connect(DAO, signers[0]) 22 | const timelock = ICompoundTimelock__factory.connect(TIMELOCK, signers[0]) 23 | 24 | await signers[0].sendTransaction({ 25 | to: MS, 26 | value: ethers.utils.parseEther("1.0") 27 | }); 28 | 29 | await hre.network.provider.request({ 30 | method: "hardhat_impersonateAccount", 31 | params: ["0xB429Bee46B7DF01D759D04D57DaBe814ECf0341b"], 32 | }); 33 | 34 | const ms = await ethers.provider.getSigner( 35 | "0xB429Bee46B7DF01D759D04D57DaBe814ECf0341b" 36 | ); 37 | 38 | let { timestamp: now } = await hre.waffle.provider.getBlock('latest') 39 | let eta = now + (await timelock.delay()).toNumber() + 1 40 | console.log("Queue pending admin to dao") 41 | await timelock.connect(ms).queueTransaction(timelock.address, 0, "setPendingAdmin(address)", "0x000000000000000000000000dbd38f7e739709fe5bfae6cc8ef67c3820830e0c", eta) 42 | 43 | await hre.network.provider.request({ 44 | method: "evm_setNextBlockTimestamp", 45 | params: [eta], 46 | }); 47 | 48 | console.log("Execute pending admin to dao") 49 | await timelock.connect(ms).executeTransaction(timelock.address, 0, "setPendingAdmin(address)", "0x000000000000000000000000dbd38f7e739709fe5bfae6cc8ef67c3820830e0c", eta) 50 | 51 | await dao.__acceptAdmin() 52 | const admin = await timelock.admin() 53 | 54 | if (admin != dao.address) { 55 | throw new Error("dao not timelock admin") 56 | } 57 | 58 | await hre.network.provider.request({ 59 | method: "hardhat_impersonateAccount", 60 | params: ["0xba740c9035fF3c24A69e0df231149c9cd12BAe07"], 61 | }); 62 | 63 | const proposer = await ethers.provider.getSigner( 64 | "0xba740c9035fF3c24A69e0df231149c9cd12BAe07" 65 | ); 66 | 67 | const sig = "receiveEth(string)" 68 | const calldata = new ethers.utils.AbiCoder().encode(["string"], ["gang"]); 69 | const treasury = await ethers.provider.getBalance(timelock.address) 70 | let txn = await dao.connect(proposer)["propose(address[],uint256[],string[],bytes[],string)"]( 71 | [receiver.address], [treasury], [sig], [calldata], "send funds") 72 | 73 | console.log(`Proposing to send ${treasury} to receiver.`) 74 | const receipt = await txn.wait() 75 | const proposalId = receipt.events![0].args!.proposalId 76 | 77 | const delay = await dao.votingDelay() 78 | const quorum = (await dao.quorumVotes()).toNumber() 79 | 80 | for (var i = 0; i < delay.toNumber(); i++) { 81 | hre.network.provider.send("evm_mine"); 82 | } 83 | 84 | await dao.connect(proposer).castVote(proposalId, 1) 85 | 86 | if (!(await dao.hasVoted(proposalId, proposer._address))) { 87 | throw new Error("proposer has voted") 88 | } 89 | 90 | let proposal = await dao.proposals(proposalId) 91 | 92 | if (proposal.forVotes.eq(await loot.balanceOf(proposer._address))) { 93 | throw new Error("proposer votes not counted for") 94 | } 95 | 96 | const voted: { [key: string]: boolean } = {} 97 | for (var i = 1; i < quorum + 1; i++) { 98 | const owner = await loot.ownerOf(i) 99 | await hre.network.provider.request({ 100 | method: "hardhat_impersonateAccount", 101 | params: [owner], 102 | }); 103 | 104 | if (voted[owner]) { 105 | continue 106 | } 107 | 108 | const balance = await ethers.provider.getBalance(owner); 109 | if (balance.eq(0)) { 110 | continue 111 | } 112 | 113 | const voter = await ethers.provider.getSigner(owner); 114 | await dao.connect(voter).castVote(proposalId, 1) 115 | voted[owner] = true 116 | } 117 | 118 | proposal = await dao.proposals(proposalId) 119 | 120 | console.log(`Voted, proposal has ${proposal.forVotes} votes for.`) 121 | 122 | try { 123 | await dao["queue(uint256)"](proposalId) 124 | throw new Error("Queueing proposal early should fail") 125 | } catch { } 126 | 127 | const block = parseInt(await hre.network.provider.send("eth_blockNumber"), 16); 128 | const toEnd = proposal.endBlock.toNumber() - block 129 | 130 | console.log(`Incrementing ${toEnd} blocks.`) 131 | for (var i = 0; i < toEnd; i++) { 132 | await hre.network.provider.send("evm_mine"); 133 | } 134 | 135 | console.log("Queueing proposal") 136 | await dao["queue(uint256)"](proposalId) 137 | 138 | try { 139 | await dao["execute(uint256)"](proposalId) 140 | throw new Error("Executing proposal early should fail") 141 | } catch { } 142 | 143 | let latest = await hre.waffle.provider.getBlock('latest') 144 | now = latest.timestamp 145 | await hre.network.provider.request({ 146 | method: "evm_setNextBlockTimestamp", 147 | params: [now + (await timelock.delay()).toNumber() + 1], 148 | }); 149 | 150 | proposal = await dao.proposals(proposalId) 151 | const state = await dao.state(proposalId) 152 | 153 | console.log("Executing proposal with status:", state) 154 | await dao["execute(uint256)"](proposalId, { 155 | gasLimit: 500000, 156 | }) 157 | 158 | const balance = await ethers.provider.getBalance(receiver.address) 159 | 160 | console.log(treasury.toString(), balance.toString()) 161 | 162 | if (!treasury.eq(balance)) { 163 | throw new Error("incorrect final balance") 164 | } 165 | } 166 | 167 | // We recommend this pattern to be able to use async/await everywhere 168 | // and properly handle errors. 169 | main() 170 | .then(() => process.exit(0)) 171 | .catch((error) => { 172 | console.error(error); 173 | process.exit(1); 174 | }); -------------------------------------------------------------------------------- /scripts/dip6.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat" 2 | 3 | import { DopeWarsLoot__factory, DopeDAO__factory, ICompoundTimelock__factory, Receiver, Receiver__factory } from "../typechain" 4 | 5 | const { parseUnits } = ethers.utils 6 | 7 | const LOOT = "0x8707276df042e89669d69a177d3da7dc78bd8723" 8 | const DAO = "0xDBd38F7e739709fe5bFaE6cc8eF67C3820830E0C" 9 | const TIMELOCK = "0xb57ab8767cae33be61ff15167134861865f7d22c" 10 | 11 | const MS = "0xB429Bee46B7DF01D759D04D57DaBe814ECf0341b" 12 | 13 | const proposalMarkdown = ` 14 | # DIP-6: Pixel Art + DIP-4 Part 1 + DIP-5 Gas Refunds 15 | 16 | A batch proposal for: 17 | 18 | [Commission Dope Wars Pixel Art](https://snapshot.org/#/dopedao.eth/proposal/Qmbhdyn31sMSu2LgFwj36747jcYbhbQWXTA8tuEv9e2NK3) 19 | **10**eth to \`0xc2407b34b19d2227addc5c6eae5c5d99432a0c99\` 20 | 21 | [DIP-4: RYO v1](https://snapshot.org/#/dopedao.eth/proposal/QmZmidDFYbvS5L7EqmL8RsqXTZ9t1yZc3MpYSwMTfJLLwY) 22 | First installment of **2.5**eth to \`0xa2701f1dadae0e1ee9fa68ab90abbda61cd9e06b\` 23 | 24 | [DIP-5: Development Gas Refund](https://www.notion.so/DIP-5-Development-Gas-Refund-a6b7e43af34e4a7682ac06a4bbe7c99d) 25 | Gas refunds for contract deployments. 26 | **2.6572230788**eth to \`0xe8d848debb3a3e12aa815b15900c8e020b863f31\` 27 | **1.15785465171**eth to \`0xba740c9035fF3c24A69e0df231149c9cd12BAe07\` 28 | ` 29 | 30 | const mrfax = "0xc2407b34b19d2227addc5c6eae5c5d99432a0c99"; 31 | const mrfaxPayout = parseUnits("10.0", "ether") 32 | const perama = "0xa2701f1dadae0e1ee9fa68ab90abbda61cd9e06b"; 33 | const peramaPayout = parseUnits("2.5", "ether") 34 | const dennison = "0xe8d848debb3a3e12aa815b15900c8e020b863f31"; 35 | const dennisonPayout = parseUnits("2.6572230788", "ether") 36 | const tarrence = "0xba740c9035fF3c24A69e0df231149c9cd12BAe07"; 37 | const tarrencePayout = parseUnits("1.15785465171", "ether") 38 | 39 | const faces = "0xa2de2d19edb4094c79fb1a285f3c30c77931bf1e"; 40 | const wolf = "0x45ba4bf71371070803bdf2c8b89e4b3eede65d99"; 41 | const shecky = "0xa9da2e4b36d75d3aee8630c6acd09fd567091f10"; 42 | 43 | async function main() { 44 | const signers = await ethers.getSigners() 45 | 46 | const loot = DopeWarsLoot__factory.connect(LOOT, signers[0]) 47 | const dao = DopeDAO__factory.connect(DAO, signers[0]) 48 | const timelock = ICompoundTimelock__factory.connect(TIMELOCK, signers[0]) 49 | 50 | await hre.network.provider.request({ 51 | method: "hardhat_impersonateAccount", 52 | params: ["0xba740c9035fF3c24A69e0df231149c9cd12BAe07"], 53 | }); 54 | 55 | const proposer = await ethers.provider.getSigner( 56 | tarrence 57 | ); 58 | 59 | let txn = await dao.connect(proposer)["propose(address[],uint256[],string[],bytes[],string)"]( 60 | [mrfax, perama, dennison, tarrence], 61 | [mrfaxPayout, peramaPayout, dennisonPayout, tarrencePayout], 62 | [], ["0x", "0x", "0x", "0x"], proposalMarkdown) 63 | 64 | const receipt = await txn.wait() 65 | const proposalId = receipt.events![0].args!.proposalId 66 | 67 | const delay = await dao.votingDelay() 68 | 69 | for (var i = 0; i < delay.toNumber(); i++) { 70 | hre.network.provider.send("evm_mine"); 71 | } 72 | 73 | await dao.connect(proposer).castVote(proposalId, 1) 74 | 75 | if (!(await dao.hasVoted(proposalId, proposer._address))) { 76 | throw new Error("proposer has voted") 77 | } 78 | 79 | let proposal = await dao.proposals(proposalId) 80 | 81 | if (proposal.forVotes.eq(await loot.balanceOf(proposer._address))) { 82 | throw new Error("proposer votes not counted for") 83 | } 84 | 85 | for (let address of [faces, wolf, shecky]) { 86 | await hre.network.provider.request({ 87 | method: "hardhat_impersonateAccount", 88 | params: [address], 89 | }); 90 | 91 | const voter = await ethers.provider.getSigner(address); 92 | await dao.connect(voter).castVote(proposalId, 1) 93 | } 94 | 95 | proposal = await dao.proposals(proposalId) 96 | 97 | console.log(`Voted, proposal has ${proposal.forVotes} votes for.`) 98 | 99 | try { 100 | await dao["queue(uint256)"](proposalId) 101 | throw new Error("Queueing proposal early should fail") 102 | } catch { } 103 | 104 | const block = parseInt(await hre.network.provider.send("eth_blockNumber"), 16); 105 | const toEnd = proposal.endBlock.toNumber() - block 106 | 107 | console.log(`Incrementing ${toEnd} blocks.`) 108 | for (var i = 0; i < toEnd; i++) { 109 | await hre.network.provider.send("evm_mine"); 110 | } 111 | 112 | console.log("Queueing proposal") 113 | await dao["queue(uint256)"](proposalId) 114 | 115 | try { 116 | await dao["execute(uint256)"](proposalId) 117 | throw new Error("Executing proposal early should fail") 118 | } catch { } 119 | 120 | let latest = await hre.waffle.provider.getBlock('latest') 121 | let now = latest.timestamp 122 | await hre.network.provider.request({ 123 | method: "evm_setNextBlockTimestamp", 124 | params: [now + (await timelock.delay()).toNumber() + 1], 125 | }); 126 | 127 | proposal = await dao.proposals(proposalId) 128 | const state = await dao.state(proposalId) 129 | 130 | const tarrenceBefore = await ethers.provider.getBalance(tarrence) 131 | 132 | console.log("Executing proposal with status:", state) 133 | await dao["execute(uint256)"](proposalId, { 134 | gasLimit: 500000, 135 | }) 136 | 137 | const tarrenceAfter = await ethers.provider.getBalance(tarrence) 138 | 139 | console.log(tarrenceBefore.toString(), tarrenceAfter.toString()) 140 | } 141 | 142 | // We recommend this pattern to be able to use async/await everywhere 143 | // and properly handle errors. 144 | main() 145 | .then(() => process.exit(0)) 146 | .catch((error) => { 147 | console.error(error); 148 | process.exit(1); 149 | }); -------------------------------------------------------------------------------- /tasks/accounts.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from "@ethersproject/abstract-signer"; 2 | import { task } from "hardhat/config"; 3 | 4 | task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => { 5 | const accounts: Signer[] = await hre.ethers.getSigners(); 6 | 7 | for (const account of accounts) { 8 | console.log(await account.getAddress()); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /tasks/clean.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra"; 2 | import { TASK_CLEAN } from "hardhat/builtin-tasks/task-names"; 3 | import { task } from "hardhat/config"; 4 | 5 | task(TASK_CLEAN, "Overrides the standard clean task", async function (_taskArgs, _hre, runSuper) { 6 | await fsExtra.remove("./coverage"); 7 | await fsExtra.remove("./coverage.json"); 8 | await runSuper(); 9 | }); 10 | -------------------------------------------------------------------------------- /tasks/deployers/index.ts: -------------------------------------------------------------------------------- 1 | import "./nftStake"; 2 | -------------------------------------------------------------------------------- /tasks/deployers/nftStake.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | import { TaskArguments } from "hardhat/types"; 3 | 4 | import { NftStake, NftStake__factory } from "../../typechain"; 5 | 6 | task("deploy:NFTStake") 7 | .addParam("nft", "The NFT Contract Address") 8 | .addParam("erc20", "The payout ERC20 Token") 9 | .addParam("dao", "The DAO which governs the contract") 10 | .addParam("reward", "ERC20 tokens rewarded per block per account") 11 | .setAction(async function (taskArguments: TaskArguments, { ethers }) { 12 | const nftStakeFactory: NftStake__factory = await ethers.getContractFactory("NftStake"); 13 | const nftStake: NftStake = ( 14 | await nftStakeFactory.deploy(taskArguments.nft, taskArguments.erc20, taskArguments.dao, taskArguments.reward) 15 | ); 16 | await nftStake.deployed(); 17 | console.log("NFT Stake deployed to: ", nftStake.address); 18 | }); 19 | -------------------------------------------------------------------------------- /test/NftStake.behavior.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { BigNumber, utils } from "ethers"; 3 | import { network } from "hardhat"; 4 | 5 | export function shouldBehaveLikeNftStake(): void { 6 | it("should let user stake NFT", async function () { 7 | // Need to approve the token first 8 | await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.be.revertedWith( 9 | "ERC721: transfer caller is not owner nor approved", 10 | ); 11 | // Approve nftStake to take the token 12 | await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, BigNumber.from(1)); 13 | // Try to stake it 14 | await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.not.be.reverted; 15 | }); 16 | 17 | it("should not let a user stake twice", async function () { 18 | // Approve nftStake to take the token 19 | await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, BigNumber.from(1)); 20 | // Try to stake it 21 | await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.not.be.reverted; 22 | // Try to stake again 23 | await expect(this.nftStake.connect(this.signers.alice).stake([BigNumber.from(1)], true)).to.be.revertedWith( 24 | "ERC721: transfer of token that is not own", 25 | ); 26 | }); 27 | 28 | it("should not let you stake a token you don't own", async function () { 29 | // Try to stake it 30 | await expect(this.nftStake.connect(this.signers.bob).stake([BigNumber.from(1)], true)).to.be.reverted; 31 | }); 32 | 33 | it("should let user unstake", async function () { 34 | const tokenId = BigNumber.from(1); 35 | // Approve nftStake to take the token 36 | await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); 37 | 38 | await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); 39 | 40 | // Try to stake it 41 | await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; 42 | 43 | // confirm nftStake owns token 44 | expect(await this.mockERC721.connect(this.signers.admin).ownerOf(tokenId)).to.eql(this.nftStake.address); 45 | 46 | // let some time pass 47 | await network.provider.send("evm_mine"); 48 | 49 | // get blocknumber user staked 50 | const startStake = (await this.nftStake.connect(this.signers.admin).receipt(tokenId)).from.toNumber(); 51 | 52 | // get current blockNumer 53 | const currentBlock = parseInt(await network.provider.send("eth_blockNumber"), 16); 54 | 55 | // estimate stake 56 | const estimatedPayout = 57 | (currentBlock - startStake) * 58 | (await (await this.nftStake.connect(this.signers.alice).emissionRate()).toNumber()); 59 | 60 | // check if estimated stake matches contract 61 | expect(await (await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eql( 62 | estimatedPayout, 63 | ); 64 | 65 | await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); 66 | 67 | // try to unstake 68 | await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], true)).to.not.be.reverted; 69 | 70 | // confirm alice owns the token again 71 | expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eql( 72 | await this.signers.alice.getAddress(), 73 | ); 74 | 75 | // check if alice has been paid the estimated stake 76 | expect( 77 | await ( 78 | await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) 79 | ).toNumber(), 80 | ).to.eql(estimatedPayout); 81 | }); 82 | 83 | it("should let user unstake in same block", async function () { 84 | const tokenId = BigNumber.from(1); 85 | // Approve nftStake to take the token 86 | await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); 87 | 88 | await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], false)).to.be.revertedWith("nftstake: must accept terms of service"); 89 | 90 | // Try to stake it 91 | await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; 92 | 93 | // confirm nftStake owns token 94 | expect(await this.mockERC721.connect(this.signers.admin).ownerOf(tokenId)).to.eql(this.nftStake.address); 95 | 96 | // check if estimated stake matches contract 97 | expect(await (await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eql( 98 | 0, 99 | ); 100 | 101 | // try to unstake 102 | await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], true)).to.not.be.reverted; 103 | 104 | // confirm alice owns the token again 105 | expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eql( 106 | await this.signers.alice.getAddress(), 107 | ); 108 | 109 | // check if alice has been paid the estimated stake 110 | expect( 111 | await ( 112 | await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) 113 | ).toNumber(), 114 | ).to.eql(0); 115 | }); 116 | 117 | it("rewardOf should return zero when not staked", async function () { 118 | const tokenId = BigNumber.from(9999); 119 | expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eql(0); 120 | }); 121 | 122 | it("rewardOf should return correct stake amount currently", async function () { 123 | const tokenId = BigNumber.from(1); 124 | 125 | // Approve nftStake to take the token 126 | await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); 127 | // Try to stake it 128 | await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; 129 | 130 | // Wait 4 blocks 131 | await network.provider.send("evm_mine"); 132 | await network.provider.send("evm_mine"); 133 | await network.provider.send("evm_mine"); 134 | await network.provider.send("evm_mine"); 135 | 136 | expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eq( 137 | 4 * this.emission, 138 | ); 139 | }); 140 | 141 | it("should allow harvesting without withdrawl", async function () { 142 | const tokenId = BigNumber.from(1); 143 | // Approve nftStake to take the token 144 | await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); 145 | // Try to stake it 146 | await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; 147 | 148 | // Wait 4 blocks 149 | await network.provider.send("evm_mine"); 150 | await network.provider.send("evm_mine"); 151 | await network.provider.send("evm_mine"); 152 | await network.provider.send("evm_mine"); 153 | 154 | // get current earned stake 155 | const currentEarnedStake = ( 156 | await this.nftStake.connect(this.signers.alice).rewardOf(tokenId) 157 | ).toNumber(); 158 | 159 | // get current token balance of user 160 | const balanceBeforeHarvest = ( 161 | await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) 162 | ).toNumber(); 163 | 164 | // get the staked receipt 165 | const stakedAtOriginal = ( 166 | await this.nftStake.connect(this.signers.alice).receipt(tokenId) 167 | ).from.toNumber(); 168 | 169 | // get current blockNumer 170 | let currentBlock = parseInt(await network.provider.send("eth_blockNumber"), 16); 171 | 172 | // check the staked receipt is 4 blocks ago 173 | expect(currentBlock - stakedAtOriginal).to.eq(4); 174 | 175 | // should have no tokens 176 | expect(balanceBeforeHarvest).to.eq(0); 177 | 178 | // should not let you harvest tokens you did not stake 179 | await expect(this.nftStake.connect(this.signers.bob).harvest([tokenId], true)).to.be.revertedWith( 180 | "nftstake: not owner", 181 | ); 182 | 183 | await expect(this.nftStake.connect(this.signers.bob).harvest([tokenId], false)).to.be.revertedWith( 184 | "nftstake: must accept terms of service", 185 | ); 186 | 187 | // harvest Stake 188 | await this.nftStake.connect(this.signers.alice).harvest([tokenId], true); 189 | 190 | // should have harvested the tokens 191 | expect( 192 | (await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress())).toNumber(), 193 | ).to.eq(currentEarnedStake); 194 | 195 | // check the new receipt 196 | const updatedStakeDate = ( 197 | await this.nftStake.connect(this.signers.alice).receipt(tokenId) 198 | ).from.toNumber(); 199 | currentBlock = parseInt(await network.provider.send("eth_blockNumber"), 16); 200 | 201 | // check the staked receipt has been updated to current blocktime 202 | expect(currentBlock).to.eq(updatedStakeDate); 203 | 204 | // check that there is no pending payout availible 205 | expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eq(0); 206 | 207 | // check that nftStake still owns the token 208 | expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eq(this.nftStake.address); 209 | 210 | // wait one block 211 | await network.provider.send("evm_mine"); 212 | 213 | // check that there is now a pending payout availible again 214 | expect((await this.nftStake.connect(this.signers.alice).rewardOf(tokenId)).toNumber()).to.eq( 215 | 1 * this.emission, 216 | ); 217 | }); 218 | 219 | it("should allow user to unstake if reward gt balance", async function () { 220 | const tokenId = BigNumber.from(1); 221 | // Approve nftStake to take the token 222 | await this.mockERC721.connect(this.signers.alice).approve(this.nftStake.address, tokenId); 223 | 224 | await this.nftStake.connect(this.signers.dao).setEmissionRate(10000) 225 | 226 | // Try to stake it 227 | await expect(this.nftStake.connect(this.signers.alice).stake([tokenId], true)).to.not.be.reverted; 228 | 229 | await network.provider.send("evm_mine"); 230 | await network.provider.send("evm_mine"); 231 | 232 | // try to unstake 233 | await expect(this.nftStake.connect(this.signers.alice).unstake([tokenId], true)).to.not.be.reverted; 234 | 235 | // confirm alice owns the token again 236 | expect(await this.mockERC721.connect(this.signers.alice).ownerOf(tokenId)).to.eql( 237 | await this.signers.alice.getAddress(), 238 | ); 239 | 240 | // check if alice has been paid the estimated stake 241 | expect( 242 | await ( 243 | await this.mockERC20.connect(this.signers.alice).balanceOf(await this.signers.alice.getAddress()) 244 | ).toNumber(), 245 | ).to.eql(0); 246 | }); 247 | } 248 | -------------------------------------------------------------------------------- /test/NftStake.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | import { Artifact } from "hardhat/types"; 3 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 4 | 5 | import { NftStake } from "../typechain"; 6 | import { MockERC20 } from "../typechain"; 7 | import { MockERC721 } from "../typechain"; 8 | 9 | import { Signers } from "./types"; 10 | 11 | // test cases 12 | import { shouldBehaveLikeNftStake } from "./NftStake.behavior"; 13 | 14 | const { deployContract } = hre.waffle; 15 | 16 | describe("Unit tests", function () { 17 | before(async function () { 18 | this.signers = {} as Signers; 19 | 20 | const signers: SignerWithAddress[] = await hre.ethers.getSigners(); 21 | this.signers.admin = signers[0]; 22 | this.signers.alice = signers[1]; 23 | this.signers.bob = signers[2]; 24 | this.signers.dao = signers[3]; 25 | }); 26 | 27 | xdescribe("NFTStake", function () { 28 | beforeEach(async function () { 29 | this.emission = 2; 30 | 31 | // deploy erc20 32 | const erc20Artifact: Artifact = await hre.artifacts.readArtifact("MockERC20"); 33 | this.mockERC20 = await deployContract(this.signers.admin, erc20Artifact, [1000]); 34 | 35 | // deploy erc721 36 | const erc721Artifact: Artifact = await hre.artifacts.readArtifact("MockERC721"); 37 | this.mockERC721 = await deployContract(this.signers.admin, erc721Artifact, []); 38 | 39 | // deploy NFTStake 40 | const nftStakeArtifact: Artifact = await hre.artifacts.readArtifact("NftStake"); 41 | this.nftStake = ( 42 | await deployContract(this.signers.admin, nftStakeArtifact, [ 43 | this.mockERC721.address, 44 | this.mockERC20.address, 45 | await this.signers.dao.getAddress(), 46 | this.emission, 47 | ]) 48 | ); 49 | 50 | // Send erc20 balance to NFTStake 51 | const adminTokenInstance: MockERC20 = await this.mockERC20.connect(this.signers.admin); 52 | await adminTokenInstance.transfer( 53 | this.nftStake.address, 54 | await adminTokenInstance.balanceOf(await this.signers.admin.getAddress()), 55 | ); 56 | 57 | // Mint some NFTS 58 | const adminERC721Instance: MockERC721 = await this.mockERC721.connect(this.signers.admin); 59 | 60 | // alice has tokenId 1 61 | // bob has tokenId 2 62 | await adminERC721Instance.mint(await this.signers.alice.getAddress(), "First"); 63 | await adminERC721Instance.mint(await this.signers.bob.getAddress(), "Second"); 64 | }); 65 | 66 | shouldBehaveLikeNftStake(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/governance/DopeDAO.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { expect } from "chai"; 3 | 4 | import { Artifact } from "hardhat/types"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 6 | 7 | import { Signers } from "../types"; 8 | import { DopeWarsLoot, DopeDAOTest, Receiver, Timelock } from "../../typechain"; 9 | 10 | const { deployContract } = hre.waffle; 11 | 12 | describe("DopeDAO", function () { 13 | before(async function () { 14 | this.signers = {} as Signers; 15 | 16 | const signers: SignerWithAddress[] = await hre.ethers.getSigners(); 17 | this.signers.admin = signers[0]; 18 | this.signers.alice = signers[1]; 19 | this.signers.bob = signers[2]; 20 | }); 21 | 22 | describe("lifecycle", function () { 23 | beforeEach(async function () { 24 | const lootArtifact: Artifact = await hre.artifacts.readArtifact("DopeWarsLoot"); 25 | this.loot = await deployContract(this.signers.admin, lootArtifact, []); 26 | 27 | const timelockArtifact: Artifact = await hre.artifacts.readArtifact("Timelock"); 28 | this.timelock = await deployContract(this.signers.admin, timelockArtifact, [this.signers.admin.address, 10]); 29 | 30 | const daoArtifact: Artifact = await hre.artifacts.readArtifact("DopeDAOTest"); 31 | this.dao = await deployContract(this.signers.admin, daoArtifact, [this.loot.address, this.timelock.address]); 32 | 33 | const receiverArtifact: Artifact = await hre.artifacts.readArtifact("Receiver"); 34 | this.receiver = await deployContract(this.signers.admin, receiverArtifact, []); 35 | 36 | await Promise.all([...Array(5).keys()].map(async (i) => this.loot.claim(i + 1))) 37 | }); 38 | 39 | it("propose and execute a proposal with no eth", async function () { 40 | let now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) 41 | const eta = now + 11; 42 | const sig = "setPendingAdmin(address)" 43 | const data = new ethers.utils.AbiCoder().encode(["address"], [this.dao.address]); 44 | 45 | await this.timelock.queueTransaction(this.timelock.address, 0, sig, data, eta); 46 | 47 | await hre.network.provider.request({ 48 | method: "evm_setNextBlockTimestamp", 49 | params: [eta], 50 | }); 51 | 52 | await this.timelock.executeTransaction(this.timelock.address, 0, sig, data, eta); 53 | await this.dao.__acceptAdmin() 54 | 55 | const calldata = new ethers.utils.AbiCoder().encode(["string"], ["gang"]); 56 | 57 | const txn = await this.dao["propose(address[],uint256[],string[],bytes[],string)"]( 58 | [this.receiver.address], [0], ["receiveNoEth(string)"], [calldata], "Send no ETH" 59 | ) 60 | 61 | const receipt = await txn.wait() 62 | const proposalId = receipt.events![0].args!.proposalId 63 | 64 | // check proposal id exists 65 | expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("0") 66 | 67 | await hre.network.provider.send("evm_mine"); 68 | 69 | await this.dao.castVote(proposalId, 1); 70 | 71 | // check we have voted 72 | expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("5") 73 | 74 | await this.dao["queue(uint256)"](proposalId); 75 | 76 | now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) 77 | await hre.network.provider.request({ 78 | method: "evm_setNextBlockTimestamp", 79 | params: [now + 11], 80 | }); 81 | 82 | await this.dao["execute(uint256)"](proposalId) 83 | 84 | // check it executed 85 | expect((await this.dao.proposals(proposalId)).executed).to.eql(true); 86 | }) 87 | 88 | it("propose and execute a proposal with eth", async function () { 89 | let now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) 90 | const eta = now + 12; 91 | const sig = "setPendingAdmin(address)" 92 | const data = new ethers.utils.AbiCoder().encode(["address"], [this.dao.address]); 93 | 94 | const value = ethers.utils.parseEther("0.1") 95 | // send eth to the timelock 96 | await this.signers.alice.sendTransaction({ 97 | to: this.timelock.address, 98 | value 99 | }) 100 | 101 | await this.timelock.queueTransaction(this.timelock.address, 0, sig, data, eta) 102 | 103 | await hre.network.provider.request({ 104 | method: "evm_setNextBlockTimestamp", 105 | params: [eta], 106 | }); 107 | 108 | await this.timelock.executeTransaction(this.timelock.address, 0, sig, data, eta) 109 | await this.dao.__acceptAdmin() 110 | 111 | const calldata = new ethers.utils.AbiCoder().encode(["string"], ["gang"]); 112 | 113 | const txn = await this.dao["propose(address[],uint256[],string[],bytes[],string)"]( 114 | [this.receiver.address], [value], ["receiveEth(string)"], [calldata], "Send ETH" 115 | ) 116 | 117 | const receipt = await txn.wait() 118 | const proposalId = receipt.events![0].args!.proposalId 119 | 120 | // check proposal id exists 121 | expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("0") 122 | 123 | await hre.network.provider.send("evm_mine"); 124 | 125 | await this.dao.castVote(proposalId, 1); 126 | 127 | // check we have voted 128 | expect((await this.dao.proposals(proposalId)).forVotes.toString()).to.eql("5") 129 | 130 | await this.dao["queue(uint256)"](proposalId); 131 | 132 | now = await hre.waffle.provider.getBlock('latest').then(block => block.timestamp) 133 | await hre.network.provider.request({ 134 | method: "evm_setNextBlockTimestamp", 135 | params: [now + 11], 136 | }); 137 | 138 | await this.dao["execute(uint256)"](proposalId) 139 | 140 | // check it executed 141 | expect((await this.dao.proposals(proposalId)).executed).to.eql(true); 142 | }) 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | import { Fixture } from "ethereum-waffle"; 3 | 4 | import { MockERC20, MockERC721, NftStake, DopeWarsLoot, DopeDAOTest, Receiver, Timelock } from "../typechain"; 5 | declare module "mocha" { 6 | export interface Context { 7 | nftStake: NftStake; 8 | mockERC20: MockERC20; 9 | mockERC721: MockERC721; 10 | emission: number; 11 | loot: DopeWarsLoot; 12 | timelock: Timelock; 13 | dao: DopeDAOTest; 14 | receiver: Receiver; 15 | loadFixture: (fixture: Fixture) => Promise; 16 | signers: Signers; 17 | } 18 | } 19 | 20 | export interface Signers { 21 | admin: SignerWithAddress; 22 | alice: SignerWithAddress; 23 | bob: SignerWithAddress; 24 | dao: SignerWithAddress; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["es6"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "outDir": "dist", 10 | "resolveJsonModule": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "target": "es6" 14 | }, 15 | "exclude": ["node_modules"], 16 | "files": ["./hardhat.config.ts"], 17 | "include": ["tasks/**/*.ts", "test/**/*.ts", "typechain/**/*.d.ts", "typechain/**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /utils/constants.ts: -------------------------------------------------------------------------------- 1 | import {BigNumber} from 'ethers'; 2 | 3 | export const MaxUint128 = BigNumber.from(2).pow(128).sub(1); 4 | 5 | export enum FeeAmount { 6 | LOW = 500, 7 | MEDIUM = 3000, 8 | HIGH = 10000, 9 | } 10 | -------------------------------------------------------------------------------- /utils/network.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | export function url(networkName: string): string { 3 | if (networkName) { 4 | const uri = process.env['ETH_NODE_URI_' + networkName.toUpperCase()]; 5 | if (uri && uri !== '') { 6 | return uri; 7 | } 8 | } 9 | 10 | if (networkName === 'localhost') { 11 | // do not use ETH_NODE_URI 12 | return 'http://localhost:8545'; 13 | } 14 | 15 | let uri = process.env.ETH_NODE_URI; 16 | if (uri) { 17 | uri = uri.replace('{{networkName}}', networkName); 18 | } 19 | if (!uri || uri === '') { 20 | // throw new Error(`environment variable "ETH_NODE_URI" not configured `); 21 | return ''; 22 | } 23 | if (uri.indexOf('{{') >= 0) { 24 | throw new Error( 25 | `invalid uri or network not supported by node provider : ${uri}` 26 | ); 27 | } 28 | return uri; 29 | } 30 | 31 | export function getMnemonic(networkName?: string): string { 32 | if (networkName) { 33 | const mnemonic = process.env['MNEMONIC_' + networkName.toUpperCase()]; 34 | if (mnemonic && mnemonic !== '') { 35 | return mnemonic; 36 | } 37 | } 38 | 39 | const mnemonic = process.env.MNEMONIC; 40 | if (!mnemonic || mnemonic === '') { 41 | return 'test test test test test test test test test test test junk'; 42 | } 43 | return mnemonic; 44 | } 45 | 46 | export function accounts(networkName?: string): { mnemonic: string } { 47 | return { mnemonic: getMnemonic(networkName) }; 48 | } 49 | --------------------------------------------------------------------------------