diff --git a/ethereum/.env.example b/ethereum/.env.example new file mode 100644 index 0000000..ceb1918 --- /dev/null +++ b/ethereum/.env.example @@ -0,0 +1 @@ +SEPOLIA_PRIVATE_KEY= \ No newline at end of file diff --git a/ethereum/contracts/DAO.sol b/ethereum/contracts/DAO.sol index 483d861..61ed4f3 100644 --- a/ethereum/contracts/DAO.sol +++ b/ethereum/contracts/DAO.sol @@ -33,11 +33,6 @@ struct ValidationPool { bool outcome; } -struct StakeData { - uint poolIndex; - bool inFavor; -} - /// This contract must manage validation pools and reputation, /// because otherwise there's no way to enforce appropriate permissions on /// transfer of value between reputation NFTs. diff --git a/ethereum/hardhat.config.js b/ethereum/hardhat.config.js index beda1d6..15b2f03 100644 --- a/ethereum/hardhat.config.js +++ b/ethereum/hardhat.config.js @@ -1,4 +1,5 @@ require('@nomicfoundation/hardhat-toolbox'); +require('@nomicfoundation/hardhat-verify'); require('dotenv').config(); /** @type import('hardhat/config').HardhatUserConfig */ @@ -16,4 +17,12 @@ module.exports = { accounts: [process.env.SEPOLIA_PRIVATE_KEY], }, }, + etherscan: { + apiKey: { + sepolia: process.env.ETHERSCAN_API_KEY, + }, + }, + sourcify: { + enabled: false, + }, }; diff --git a/ethereum/package-lock.json b/ethereum/package-lock.json index 762ad08..ec8ab21 100644 --- a/ethereum/package-lock.json +++ b/ethereum/package-lock.json @@ -15,6 +15,7 @@ "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.5", "chai": "^4.4.1", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", @@ -1693,11 +1694,10 @@ } }, "node_modules/@nomicfoundation/hardhat-verify": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.4.tgz", - "integrity": "sha512-B8ZjhOrmbbRWqJi65jvQblzjsfYktjqj2vmOm+oc2Vu8drZbT2cjeSCRHZKbS7lOtfW78aJZSFvw+zRLCiABJA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.5.tgz", + "integrity": "sha512-Tg4zu8RkWpyADSFIgF4FlJIUEI4VkxcvELsmbJn2OokbvH2SnUrqKmw0BBfDrtvP0hhmx8wsnrRKP5DV/oTyTA==", "dev": true, - "peer": true, "dependencies": { "@ethersproject/abi": "^5.1.2", "@ethersproject/address": "^5.0.2", @@ -2389,7 +2389,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -2740,7 +2739,6 @@ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -3120,7 +3118,6 @@ "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", "dev": true, - "peer": true, "dependencies": { "nofilter": "^3.1.0" }, @@ -6396,8 +6393,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -6556,8 +6552,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -6576,8 +6571,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -7118,7 +7112,6 @@ "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", "dev": true, - "peer": true, "engines": { "node": ">=12.19" } @@ -8286,7 +8279,6 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -8304,7 +8296,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -8320,7 +8311,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -8332,8 +8322,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/solc": { "version": "0.7.3", @@ -8751,7 +8740,6 @@ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, - "peer": true, "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", diff --git a/ethereum/package.json b/ethereum/package.json index 2ba1464..d9cc49c 100644 --- a/ethereum/package.json +++ b/ethereum/package.json @@ -4,13 +4,16 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "hardhat test", + "automatic-staking-local": "hardhat run --network localhost scripts/automatic-staking.js localhost", + "automatic-staking-sepolia": "hardhat run --network sepolia scripts/automatic-staking.js sepolia" }, "author": "", "license": "ISC", "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.5", "chai": "^4.4.1", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", diff --git a/ethereum/scripts/automatic-staking.js b/ethereum/scripts/automatic-staking.js new file mode 100644 index 0000000..f63b239 --- /dev/null +++ b/ethereum/scripts/automatic-staking.js @@ -0,0 +1,111 @@ +const { ethers } = require('hardhat'); + +const DAOAddress = { + localhost: '0x76Dfe9F47f06112a1b78960bf37d87CfbB6D6133', + sepolia: '0x39B7522Ee1A5B13aE5580C40114239D4cE0e7D29', +}; + +let dao; +let account; +let validationPools; +let reputation; + +const fetchReputation = async () => { + reputation = await dao.balanceOf(account); + console.log(`reputation: ${reputation}`); +}; + +const fetchValidationPool = async (poolIndex) => { + const pool = await dao.validationPools(poolIndex); + validationPools[poolIndex] = pool; + return pool; +}; + +const fetchValidationPools = async () => { + const count = await dao.validationPoolCount(); + console.log(`validation pool count: ${count}`); + const promises = []; + validationPools = []; + for (let i = 0; i < count; i += 1) { + promises.push(fetchValidationPool(i)); + } + await Promise.all(promises); +}; + +const initialize = async () => { + const network = process.argv[2]; + if (!network) { + throw new Error('please provide network name as first argument'); + } + if (!DAOAddress[network]) { + throw new Error(`network '${network}' is unknown`); + } + dao = await ethers.getContractAt('DAO', DAOAddress); + [account] = await ethers.getSigners(); + const address = await account.getAddress(); + console.log(`account: ${address}`); + await fetchReputation(); + await fetchValidationPools(); +}; + +const poolIsActive = (pool) => { + if (new Date() >= new Date(Number(pool.endTime) * 1000)) return false; + if (pool.resolved) return false; + return true; +}; + +const stake = async (pool, amount) => { + console.log(`staking ${amount} in favor of pool ${pool.id.toString()}`); + await dao.stake(pool.id, amount, true); + await fetchReputation(); +}; + +const stakeEach = async (pools, amountPerPool) => { + const promises = []; + pools.forEach((pool) => { + promises.push(stake(pool, amountPerPool)); + }); + await Promise.all(promises); +}; + +async function main() { + await initialize(); + + validationPools.forEach((pool) => { + let status; + if (poolIsActive(pool)) status = 'Active'; + else if (!pool.resolved) status = 'Ready to Evaluate'; + else if (pool.outcome) status = 'Accepted'; + else status = 'Rejected'; + console.log(`pool ${pool.id.toString()}, status: ${status}`); + }); + + // Stake half of available reputation on any active validation pools + const activePools = validationPools.filter(poolIsActive); + if (activePools.length && reputation > 0) { + const amountPerPool = reputation / BigInt(2) / BigInt(activePools.length); + await stakeEach(activePools, amountPerPool); + } + + // Listen for new validation pools + dao.on('ValidationPoolInitiated', async (poolIndex) => { + console.log(`pool ${poolIndex} started`); + await fetchValidationPool(poolIndex); + await fetchReputation(); + + // Stake half of available reputation on this validation pool + const amount = reputation / BigInt(2); + await stake(poolIndex, amount, true); + }); + + dao.on('ValidationPoolResolved', async (poolIndex, votePasses) => { + console.log(`pool ${poolIndex} resolved, status: ${votePasses ? 'accepted' : 'rejected'}`); + fetchValidationPool(poolIndex); + fetchReputation(); + }); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/ethereum/test/DAO.js b/ethereum/test/DAO.js index 1815dac..23b86e1 100644 --- a/ethereum/test/DAO.js +++ b/ethereum/test/DAO.js @@ -44,9 +44,7 @@ describe('DAO', () => { const POOL_FEE = 100; beforeEach(async () => { - const setup = await loadFixture(deploy); - dao = setup.dao; - account1 = setup.account1; + ({ dao, account1 } = await loadFixture(deploy)); await dao.addPost(account1); const init = () => dao.initiateValidationPool(0, POOL_DURATION, { value: POOL_FEE }); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0); diff --git a/ethereum/test/Work1.js b/ethereum/test/Work1.js index 8e25c3f..c9f458d 100644 --- a/ethereum/test/Work1.js +++ b/ethereum/test/Work1.js @@ -44,11 +44,9 @@ describe('Work1', () => { let account2; beforeEach(async () => { - const setup = await loadFixture(deploy); - dao = setup.dao; - work1 = setup.work1; - account1 = setup.account1; - account2 = setup.account2; + ({ + dao, work1, account1, account2, + } = await loadFixture(deploy)); await expect(dao.stakeAvailability(work1.target, 50, STAKE_DURATION)).to.emit(work1, 'AvailabilityStaked').withArgs(0); }); @@ -204,11 +202,9 @@ describe('Work1', () => { let account2; beforeEach(async () => { - const setup = await loadFixture(deploy); - dao = setup.dao; - work1 = setup.work1; - account1 = setup.account1; - account2 = setup.account2; + ({ + dao, work1, account1, account2, + } = await loadFixture(deploy)); await dao.stakeAvailability(work1.target, 50, STAKE_DURATION); });