diff --git a/ethereum/contracts/DAO.sol b/ethereum/contracts/DAO.sol index c8f63bb..d36cc62 100644 --- a/ethereum/contracts/DAO.sol +++ b/ethereum/contracts/DAO.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.24; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "./ReputationHolder.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; struct Stake { bool inFavor; uint256 amount; address sender; - uint256 tokenId; } struct ValidationPool { @@ -16,12 +14,12 @@ struct ValidationPool { uint stakeCount; address author; uint256 fee; + uint256 initialStakedFor; + uint256 initialStakedAgainst; uint duration; uint endTime; bool resolved; bool outcome; - uint256 tokenIdFor; - uint256 tokenIdAgainst; } struct StakeData { @@ -32,10 +30,10 @@ struct StakeData { /// 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. -contract DAO is ERC721("Reputation", "REP"), ReputationHolder { - mapping(uint256 tokenId => uint256) tokenValues; - uint256 nextTokenId; - uint256 public totalValue; +contract DAO is ERC20("Reputation", "REP") { + mapping(uint => address) public members; + uint public memberCount; + mapping(address => bool) public isMember; mapping(uint => ValidationPool) public validationPools; uint public validationPoolCount; @@ -47,53 +45,11 @@ contract DAO is ERC721("Reputation", "REP"), ReputationHolder { // TODO: Add forum parameters event ValidationPoolInitiated(uint poolIndex); - event ValidationPoolResolved(bool votePasses, uint256 newTokenId); - - /// Inspect the value of a given reputation NFT - function valueOf(uint256 tokenId) public view returns (uint256 value) { - value = tokenValues[tokenId]; - } - - /// Confirm ownership of a token and return its value. - /// This should be used when receiving an NFT transfer, because otherwise - /// someone could send any NFT with a tokenId matching one of ours. - function verifiedValueOf( - address owner, - uint256 tokenId - ) public view returns (uint256 value) { - require(ownerOf(tokenId) == owner, "NFT owner mismatch"); - value = valueOf(tokenId); - } - - /// Internal function to mint a new reputation NFT - function mint(uint256 value) internal returns (uint256 tokenId) { - // Generate a new (sequential) ID for the token - tokenId = nextTokenId++; - // Mint the token, initially to be owned by the current contract. - _mint(address(this), tokenId); - tokenValues[tokenId] = value; - // Keep track of total value minted - // TODO: More sophisticated logic can compute total _available_, _active_ reputation - totalValue += value; - } - - /// Internal function to transfer value from one reputation token to another - function transferValueFrom( - uint256 fromTokenId, - uint256 toTokenId, - uint256 amount - ) internal { - require(amount >= 0, "Value transfer amount must be positive"); - require( - valueOf(fromTokenId) >= amount, - "Source token has insufficient value" - ); - tokenValues[fromTokenId] -= amount; - tokenValues[toTokenId] += amount; - } + event ValidationPoolResolved(bool votePasses); /// Accept fee to initiate a validation pool /// TODO: Rather than accept author as a parameter, accept a reference to a forum post + /// TODO: Handle multiple authors /// TODO: Constrain duration to allowable range function initiateValidationPool( address author, @@ -112,42 +68,30 @@ contract DAO is ERC721("Reputation", "REP"), ReputationHolder { // Implementing this with adjustable parameters will require more advanced fixed point math. // TODO: Make minting ratio an adjustable parameter // TODO: Make stakeForAuthor an adjustable parameter - pool.tokenIdFor = mint(msg.value / 2); - pool.tokenIdAgainst = mint(msg.value / 2); - stake(pool, address(this), true, pool.tokenIdFor); - stake(pool, address(this), false, pool.tokenIdAgainst); + _mint(address(this), msg.value); + _stake(pool, author, msg.value / 2, true); + _stake(pool, author, msg.value / 2, false); emit ValidationPoolInitiated(poolIndex); } /// Internal function to register a stake for/against a validation pool - function stake( + function _stake( ValidationPool storage pool, address sender, - bool inFavor, - uint256 tokenId + uint256 amount, + bool inFavor ) internal { require(block.timestamp <= pool.endTime, "Pool end time has passed"); - Stake storage _stake = pool.stakes[pool.stakeCount++]; - _stake.sender = sender; - _stake.inFavor = inFavor; - _stake.amount = verifiedValueOf(sender, tokenId); - _stake.tokenId = tokenId; + Stake storage s = pool.stakes[pool.stakeCount++]; + s.sender = sender; + s.inFavor = inFavor; + s.amount = amount; } /// Accept reputation stakes toward a validation pool - function onERC721Received( - address, - address from, - uint256 tokenId, - bytes calldata data - ) public override returns (bytes4) { - // `data` needs to encode the target validation pool, and the for/again boolean - StakeData memory stakeParameters = abi.decode(data, (StakeData)); - ValidationPool storage pool = validationPools[ - stakeParameters.poolIndex - ]; - stake(pool, from, stakeParameters.inFavor, tokenId); - return super.onERC721Received.selector; + function stake(uint poolIndex, uint256 amount, bool inFavor) public { + ValidationPool storage pool = validationPools[poolIndex]; + _stake(pool, msg.sender, amount, inFavor); } /// Evaluate outcome of a validation pool @@ -158,15 +102,15 @@ contract DAO is ERC721("Reputation", "REP"), ReputationHolder { "Pool end time has not yet arrived" ); require(pool.resolved == false, "Pool is already resolved"); - uint256 amountFor; - uint256 amountAgainst; - Stake storage _stake; + uint256 stakedFor; + uint256 stakedAgainst; + Stake storage s; for (uint i = 0; i < pool.stakeCount; i++) { - _stake = pool.stakes[i]; - if (_stake.inFavor) { - amountFor += _stake.amount; + s = pool.stakes[i]; + if (s.inFavor) { + stakedFor += s.amount; } else { - amountAgainst += _stake.amount; + stakedAgainst += s.amount; } } // Here we assume a quorum of 0 @@ -174,82 +118,30 @@ contract DAO is ERC721("Reputation", "REP"), ReputationHolder { // A tie is resolved in favor of the validation pool. // This is especially important so that the DAO's first pool can pass, // when no reputation has yet been minted. - votePasses = amountFor >= amountAgainst; + votePasses = stakedFor >= stakedAgainst; + if (votePasses && !isMember[pool.author]) { + members[memberCount++] = pool.author; + isMember[pool.author] = true; + } pool.resolved = true; - emit ValidationPoolResolved(votePasses, pool.tokenIdFor); - // If the outcome is true, value of all stakes against the pool should be distributed among the stakes in favor. - // If the outcome is false, value of all stakes for the pool should be distributed among the stakes against. - uint256 amountFromWinners; - uint256 amountFromLosers; - // Collect the reputation from the losing stakes + emit ValidationPoolResolved(votePasses); + // Value of losing stakes should be distributed among winners, in proportion to their stakes + uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst; + uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor; for (uint i = 0; i < pool.stakeCount; i++) { - _stake = pool.stakes[i]; - if (votePasses && !_stake.inFavor) { - // Transfer value to the token that was minted in favor - amountFromLosers += _stake.amount; - transferValueFrom( - _stake.tokenId, - pool.tokenIdFor, - _stake.amount - ); - } else if (!votePasses && _stake.inFavor) { - // Transfer value to the token that was minted against - amountFromLosers += _stake.amount; - transferValueFrom( - _stake.tokenId, - pool.tokenIdAgainst, - _stake.amount - ); - } else if ( - votePasses && - _stake.inFavor && - _stake.tokenId != pool.tokenIdFor - ) { - // Tally the total value of winning stakes - amountFromWinners += _stake.amount; - } else if ( - !votePasses && - !_stake.inFavor && - _stake.tokenId != pool.tokenIdAgainst - ) { - // Tally the total value of winning stakes - amountFromWinners += _stake.amount; - } - } - // Distribute reputation from losing stakes to winning stakes - for (uint i = 0; i < pool.stakeCount; i++) { - _stake = pool.stakes[i]; - if ( - votePasses && - _stake.inFavor && - _stake.tokenId != pool.tokenIdFor - ) { - uint256 reward = (amountFromLosers * _stake.amount) / + s = pool.stakes[i]; + if (votePasses == s.inFavor) { + uint256 reward = (amountFromLosers * s.amount) / amountFromWinners; - transferValueFrom(pool.tokenIdAgainst, _stake.tokenId, reward); - } else if ( - !votePasses && - !_stake.inFavor && - _stake.tokenId != pool.tokenIdAgainst - ) { - uint256 reward = (amountFromLosers * _stake.amount) / - amountFromWinners; - transferValueFrom(pool.tokenIdFor, _stake.tokenId, reward); + _transfer(address(this), s.sender, s.amount + reward); } } - // Transfer minted reputation to the author - // TODO: Handle multiple authors - if (votePasses) { - _transfer(address(this), pool.author, pool.tokenIdFor); - } // Distribute fee proportionatly among all reputation holders - for (uint tokenId = 0; tokenId < nextTokenId; tokenId++) { - address recipient = ownerOf(tokenId); - // Don't count tokens owned by this contract, as these are part of validation pools in progress - if (recipient == address(this)) continue; - uint256 share = (pool.fee * valueOf(tokenId)) / totalValue; + for (uint i = 0; i < memberCount; i++) { + address member = members[i]; + uint256 share = (pool.fee * balanceOf(member)) / totalSupply(); // TODO: For efficiency this could be modified to hold the funds for recipients to withdraw - payable(recipient).transfer(share); + payable(member).transfer(share); } } } diff --git a/ethereum/contracts/Work1.sol b/ethereum/contracts/Work1.sol index a0db44b..11e9600 100644 --- a/ethereum/contracts/Work1.sol +++ b/ethereum/contracts/Work1.sol @@ -6,7 +6,6 @@ import "./ReputationHolder.sol"; struct AvailabilityStake { address worker; - uint256 tokenId; uint256 amount; bool assigned; } @@ -46,18 +45,13 @@ contract Work1 is ReputationHolder { } /// Accept availability stakes as reputation token transfer - function onERC721Received( - address, - address from, - uint256 tokenId, - bytes calldata - ) public override returns (bytes4) { + function stakeAvailability(uint256 amount) public { + require(dao.balanceOf(msg.sender) >= amount); AvailabilityStake storage stake = stakes[stakeCount++]; - stake.worker = from; - stake.tokenId = tokenId; - stake.amount = dao.verifiedValueOf(from, tokenId); - // TODO: use `data` parameter to include stake options such as duration - return super.onERC721Received.selector; + stake.worker = msg.sender; + stake.amount = amount; + // TODO: Token locking + // TODO: Duration } /// Select a worker randomly from among the available workers, weighted by amount staked diff --git a/ethereum/test/DAO.js b/ethereum/test/DAO.js index 518b973..db1ac8d 100644 --- a/ethereum/test/DAO.js +++ b/ethereum/test/DAO.js @@ -38,7 +38,8 @@ describe('DAO', () => { const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: fee }); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0); expect(await dao.validationPoolCount()).to.equal(1); - expect(await dao.ownerOf(0)).to.equal(dao.target); + expect(await dao.memberCount()).to.equal(0); + expect(await dao.balanceOf(account1)).to.equal(0); }); it('should not be able to initiate a validation pool without a fee', async () => { @@ -65,13 +66,14 @@ describe('DAO', () => { it('should be able to evaluate outcome after duration has elapsed', async () => { time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(true, 0); - expect(await dao.ownerOf(0)).to.equal(account1); + await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(true); + expect(await dao.memberCount()).to.equal(1); + expect(await dao.balanceOf(account1)).to.equal(100); }); it('should not be able to evaluate outcome more than once', async () => { time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(true, 0); + await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(true); await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool is already resolved'); }); @@ -80,8 +82,10 @@ describe('DAO', () => { await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); expect(await dao.validationPoolCount()).to.equal(2); time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(true, 2); - expect(await dao.ownerOf(2)).to.equal(account1); + await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(true); + expect(await dao.balanceOf(account1)).to.equal(100); + await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(true); + expect(await dao.balanceOf(account1)).to.equal(200); }); }); });