staking
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 37s Details

This commit is contained in:
Ladd Hoffman 2024-03-11 13:39:56 -05:00
parent f59bfc3c9c
commit 71a1d3b529
4 changed files with 126 additions and 48 deletions

View File

@ -4,6 +4,8 @@ pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./IAcceptAvailability.sol";
import "hardhat/console.sol";
struct Stake {
bool inFavor;
uint256 amount;
@ -46,7 +48,7 @@ contract DAO is ERC20("Reputation", "REP") {
// TODO: Add forum parameters
event ValidationPoolInitiated(uint poolIndex);
event ValidationPoolResolved(bool votePasses);
event ValidationPoolResolved(uint poolIndex, bool votePasses);
/// Accept fee to initiate a validation pool
/// TODO: Rather than accept author as a parameter, accept a reference to a forum post
@ -69,7 +71,7 @@ contract DAO is ERC20("Reputation", "REP") {
// 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
_mint(address(this), msg.value);
_mint(author, msg.value);
// TODO: We need a way to exclude this pending reputation from the total supply when computing fee distribution
_stake(pool, author, msg.value / 2, true);
_stake(pool, author, msg.value / 2, false);
@ -84,6 +86,7 @@ contract DAO is ERC20("Reputation", "REP") {
bool inFavor
) internal {
require(block.timestamp <= pool.endTime, "Pool end time has passed");
_transfer(sender, address(this), amount);
Stake storage s = pool.stakes[pool.stakeCount++];
s.sender = sender;
s.inFavor = inFavor;
@ -126,18 +129,25 @@ contract DAO is ERC20("Reputation", "REP") {
isMember[pool.author] = true;
}
pool.resolved = true;
emit ValidationPoolResolved(votePasses);
// Value of losing stakes should be distributed among winners, in proportion to their stakes
emit ValidationPoolResolved(poolIndex, votePasses);
// Value of losing stakes should be di stributed among winners, in proportion to their stakes
uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst;
uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor;
uint256 totalRewards;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
if (votePasses == s.inFavor) {
uint256 reward = (amountFromLosers * s.amount) /
amountFromWinners;
_transfer(address(this), s.sender, s.amount + reward);
totalRewards += reward;
}
}
// Due to rounding, there may be some reward left over. Include this as a reward to the author.
uint256 remainder = amountFromLosers - totalRewards;
if (remainder > 0) {
_transfer(address(this), pool.author, remainder);
}
// Distribute fee proportionatly among all reputation holders
for (uint i = 0; i < memberCount; i++) {
address member = members[i];

View File

@ -7,8 +7,9 @@ import "./IAcceptAvailability.sol";
struct AvailabilityStake {
address worker;
uint256 amount;
uint duration;
uint endTime;
bool assigned;
bool reclaimed;
}
enum WorkStatus {
@ -54,7 +55,31 @@ contract Work1 is IAcceptAvailability {
AvailabilityStake storage stake = stakes[stakeCount++];
stake.worker = sender;
stake.amount = amount;
stake.duration = duration;
stake.endTime = block.timestamp + duration;
}
function extendAvailability(uint stakeIndex, uint duration) external {
AvailabilityStake storage stake = stakes[stakeIndex];
require(
msg.sender == stake.worker,
"Worker can only extend their own availability stake"
);
stake.endTime = block.timestamp + duration;
}
function reclaimAvailability(uint stakeIndex) external {
AvailabilityStake storage stake = stakes[stakeIndex];
require(
msg.sender == stake.worker,
"Worker can only reclaim their own availability stake"
);
require(
block.timestamp > stake.endTime,
"Stake duration has not yet elapsed"
);
require(!stake.reclaimed, "Stake has already been reclaimed");
stake.reclaimed = true;
dao.transfer(msg.sender, stake.amount);
}
/// Select a worker randomly from among the available workers, weighted by amount staked
@ -62,12 +87,14 @@ contract Work1 is IAcceptAvailability {
uint totalStakes;
for (uint i = 0; i < stakeCount; i++) {
if (stakes[i].assigned) continue;
if (block.timestamp > stakes[i].endTime) continue;
totalStakes += stakes[i].amount;
}
uint select = block.prevrandao % totalStakes;
uint acc;
for (uint i = 0; i < stakeCount; i++) {
if (stakes[i].assigned) continue;
if (block.timestamp > stakes[i].endTime) continue;
acc += stakes[i].amount;
if (acc > select) return i;
}

View File

@ -22,71 +22,111 @@ describe('DAO', () => {
it('Should deploy', async () => {
const { dao } = await loadFixture(deploy);
expect(dao).to.exist;
expect(await dao.totalValue()).to.equal(0);
expect(await dao.totalSupply()).to.equal(0);
});
describe('Validation Pool', () => {
let dao;
let account1;
const POOL_DURATION = 3600; // 1 hour
const fee = 100;
const POOL_FEE = 100;
beforeEach(async () => {
const setup = await loadFixture(deploy);
dao = setup.dao;
account1 = setup.account1;
const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: fee });
const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: POOL_FEE });
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0);
expect(await dao.validationPoolCount()).to.equal(1);
expect(await dao.memberCount()).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(0);
expect(await dao.totalSupply()).to.equal(fee);
expect(await dao.totalSupply()).to.equal(POOL_FEE);
});
it('should not be able to initiate a validation pool without a fee', async () => {
const setup = await loadFixture(deploy);
const init = () => setup.dao.initiateValidationPool(setup.account1, POOL_DURATION);
await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool');
describe('Initiate', () => {
it('should not be able to initiate a validation pool without a fee', async () => {
const setup = await loadFixture(deploy);
const init = () => setup.dao.initiateValidationPool(setup.account1, POOL_DURATION);
await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool');
});
it('should be able to initiate a second validation pool', async () => {
const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: POOL_FEE });
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
expect(await dao.validationPoolCount()).to.equal(2);
});
it('Should be able to fetch pool instance', async () => {
const pool = await dao.validationPools(0);
expect(pool).to.exist;
expect(pool.duration).to.equal(POOL_DURATION);
});
});
it('should be able to initiate a second validation pool', async () => {
const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: fee });
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
expect(await dao.validationPoolCount()).to.equal(2);
describe('Evaluate outcome', () => {
it('should not be able to evaluate outcome before duration has elapsed', async () => {
await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool end time has not yet arrived');
});
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(0, 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(0, true);
await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool is already resolved');
});
it('should be able to evaluate outcome of second validation pool', async () => {
const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: POOL_FEE });
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(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true);
expect(await dao.balanceOf(account1)).to.equal(100);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true);
expect(await dao.balanceOf(account1)).to.equal(200);
});
});
it('Should be able to fetch pool instance', async () => {
const pool = await dao.validationPools(0);
expect(pool).to.exist;
expect(pool.duration).to.equal(POOL_DURATION);
});
describe('Stake', async () => {
beforeEach(async () => {
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(0);
expect(await dao.balanceOf(account1)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
await dao.initiateValidationPool(account1, POOL_DURATION, { value: POOL_FEE });
expect(await dao.balanceOf(dao.target)).to.equal(100);
});
it('should not be able to evaluate outcome before duration has elapsed', async () => {
await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool end time has not yet arrived');
});
it('should be able to stake before validation pool has elapsed', async () => {
await dao.stake(1, 10, true);
expect(await dao.balanceOf(account1)).to.equal(90);
expect(await dao.balanceOf(dao.target)).to.equal(110);
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(200);
});
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);
expect(await dao.memberCount()).to.equal(1);
expect(await dao.balanceOf(account1)).to.equal(100);
});
it('should not be able to stake after validation pool has elapsed', async () => {
time.increase(POOL_DURATION + 1);
await expect(dao.stake(1, 10, true)).to.be.revertedWith('Pool end time has passed');
});
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);
await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool is already resolved');
});
it('should be able to evaluate outcome of second validation pool', async () => {
const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: fee });
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(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);
it('should be able to stake against a validation pool', async () => {
await dao.stake(1, 10, false);
expect(await dao.balanceOf(account1)).to.equal(90);
expect(await dao.balanceOf(dao.target)).to.equal(110);
time.increase(POOL_DURATION + 1);
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.balanceOf(account1)).to.equal(200);
});
});
});
});

View File

@ -1,4 +1,5 @@
const {
time,
loadFixture,
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
const { expect } = require('chai');
@ -42,6 +43,6 @@ describe('Work1', () => {
const stake = await work1.stakes(0);
expect(stake.worker).to.equal(account1);
expect(stake.amount).to.equal(50);
expect(stake.duration).to.equal(60);
expect(stake.endTime).to.equal(await time.latest() + 60);
});
});