small fixes
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 32s Details

This commit is contained in:
Ladd Hoffman 2024-03-29 18:08:30 -05:00
parent 3c839a8546
commit 046aba2c48
12 changed files with 365 additions and 143 deletions

View File

@ -121,9 +121,9 @@ function App() {
};
const fetchValidationPools = async () => {
// TODO: Pagination
// TODO: Memoization
// TODO: Caching
// TODO: Pagination
// TODO: Memoization
// TODO: Caching
const count = await DAOContract.methods.validationPoolCount().call();
const promises = [];
dispatchValidationPool({ type: 'refresh' });
@ -234,7 +234,7 @@ function App() {
).send({
from: account,
gas: 1000000,
value: 100,
value: 10000,
});
}, [provider, DAO, account]);
@ -274,10 +274,15 @@ function App() {
/* --------------------------- END UI ACTIONS ------------------------------------- */
/* -------------------------------------------------------------------------------- */
const getAdressName = useCallback((address) => {
const getAddressName = useCallback((address) => {
const contractName = getContractNameByAddress(chainId, address);
if (contractName) return `${contractName} Contract`;
return address;
const addressParts = [
address.slice(0, 7),
address.slice(address.length - 5),
];
return addressParts.join('...');
}, [chainId]);
return (
@ -355,8 +360,8 @@ function App() {
{posts.filter((x) => !!x).map((post) => (
<tr key={post.id}>
<td>{post.id.toString()}</td>
<td>{post.author}</td>
<td>{getAdressName(post.sender)}</td>
<td>{getAddressName(post.author)}</td>
<td>{getAddressName(post.sender)}</td>
<td>
<Button onClick={() => handleShowViewPost(post)}>
View Post
@ -393,6 +398,14 @@ function App() {
<th>Post ID</th>
<th>Sender</th>
<th>Fee</th>
<th>Binding</th>
<th>Quorum</th>
<th>WinRatio</th>
<th>
Redistribute
<br />
Losing Stakes
</th>
<th>Duration</th>
<th>End Time</th>
<th>
@ -409,9 +422,16 @@ function App() {
<tr key={pool.id}>
<td>{pool.id.toString()}</td>
<td>{pool.postIndex.toString()}</td>
<td>{getAdressName(pool.sender)}</td>
<td>{getAddressName(pool.sender)}</td>
<td>{pool.fee.toString()}</td>
<td>{pool.duration.toString()}</td>
<td>
{pool.params.bindingPercent.toString()}
%
</td>
<td>{`${pool.params.quorum[0].toString()} / ${pool.params.quorum[1].toString()}`}</td>
<td>{`${pool.params.winRatio[0].toString()} / ${pool.params.winRatio[1].toString()}`}</td>
<td>{pool.params.redistributeLosingStakes.toString()}</td>
<td>{pool.params.duration.toString()}</td>
<td>{new Date(Number(pool.endTime) * 1000).toLocaleString()}</td>
<td>{pool.stakeCount.toString()}</td>
<td>{pool.status}</td>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,12 +4,25 @@ import {
import { Web3 } from 'web3';
import Button from 'react-bootstrap/esm/Button';
import Stack from 'react-bootstrap/esm/Stack';
import useList from '../utils/List';
import Web3Context from '../contexts/Web3Context';
import ProposalsArtifact from '../assets/Proposals.json';
import { getContractAddressByChainId } from '../utils/contract-config';
import AddPostModal from './posts/AddPostModal';
const getProposalStatus = (proposal) => {
switch (Number(proposal.stage)) {
case 0: return 'Proposal';
case 1: return 'Referendum 0%';
case 2: return 'Referendum 1%';
case 3: return 'Referendum 100%';
case 4: return 'Failed';
case 5: return 'Accepted';
default: return 'Unknown';
}
};
function Proposals() {
const {
provider, chainId, account, reputation,
@ -20,6 +33,7 @@ function Proposals() {
const fetchProposal = useCallback(async (proposalIndex) => {
const proposal = await proposalsContract.current.methods.proposals(proposalIndex).call();
proposal.pools = await proposalsContract.current.methods.getPools(proposalIndex).call();
dispatchProposal({
type: 'update',
item: {
@ -28,6 +42,7 @@ function Proposals() {
},
});
console.log('proposal.pools', proposal.pools);
}, [proposalsContract, dispatchProposal]);
const fetchProposals = useCallback(async () => {
@ -41,9 +56,8 @@ function Proposals() {
}, [proposalsContract, fetchProposal, dispatchProposal]);
// Initial data load and event subscription
// TODO: unsubscribe on unmount
useEffect(() => {
if (chainId === undefined) return;
if (chainId === undefined) return () => {};
const web3 = new Web3(provider);
const ProposalsAddress = getContractAddressByChainId(chainId, 'Proposals');
const contract = new web3.eth.Contract(ProposalsArtifact.abi, ProposalsAddress);
@ -51,10 +65,17 @@ function Proposals() {
fetchProposals();
contract.events.NewProposal({ fromBlock: 'latest' }).on('data', (event) => {
const onNewProposal = (event) => {
console.log('event: new proposal', event);
fetchProposal(event.returnValues.proposalIndex);
});
};
contract.events.NewProposal({ fromBlock: 'latest' }).on('data', onNewProposal);
// unsubscribe on unmount
return () => {
contract.events.NewProposal().off(onNewProposal);
};
}, [provider, chainId, proposalsContract, fetchProposals, fetchProposal]);
const handleShowAddProposal = () => setShowAddProposal(true);
@ -82,6 +103,38 @@ function Proposals() {
});
}, [proposalsContract, account]);
const getReferenda = (proposal) => {
if (!proposal || !proposal.pools) return [];
console.log('proposal.pools', proposal.pools);
const referenda = [];
for (let referendumIndex = 0; referendumIndex < 3; referendumIndex += 1) {
for (let i = 0; i < 3; i += 1) {
const pool = proposal.pools[referendumIndex][i];
if (pool.started) {
console.log('pool', pool);
referenda.push(
<div key={`${referendumIndex}.{i}`}>
{`${referendumIndex}.${i}. `}
{!pool.completed && (
<span>
In Progress
</span>
)}
{pool.completed && (
<span>
{`${pool.stakedFor.toString()} U / ${pool.stakedAgainst.toString()} D`}
{', '}
{pool.votePasses ? 'Accepted' : 'Rejected'}
{pool.quorumMet || ': Quorum not met'}
</span>
)}
</div>,
);
}
}
}
return referenda;
};
return (
<>
<AddPostModal title="New Proposal" show={showAddProposal} setShow={setShowAddProposal} onSubmit={onSubmitProposal} />
@ -97,22 +150,28 @@ function Proposals() {
<th>Fee</th>
<th>Stage</th>
<th>Attestation</th>
<th>Referenda</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{proposals.filter((x) => !!x).map((request) => (
<tr key={request.id}>
<td>{request.id.toString()}</td>
<td>{request.fee.toString()}</td>
<td>{request.stage.toString()}</td>
<td>{request.attestationTotal.toString()}</td>
{proposals.filter((x) => !!x).map((proposal) => (
<tr key={proposal.id}>
<td>{proposal.id.toString()}</td>
<td>{proposal.fee.toString()}</td>
<td>{getProposalStatus(proposal)}</td>
<td>{proposal.attestationTotal.toString()}</td>
<td>
{request.stage === 0n && (
<Stack direction="vertical">
{getReferenda(proposal)}
</Stack>
</td>
<td>
{proposal.stage === 0n && (
<>
<Button onClick={() => handleAttest(request.id)}>Attest</Button>
<Button onClick={() => handleAttest(proposal.id)}>Attest</Button>
{' '}
<Button onClick={() => handleEvaluateAttestation(request.id)}>
<Button onClick={() => handleEvaluateAttestation(proposal.id)}>
Evaluate Attestation
</Button>
</>

View File

@ -1,9 +1,9 @@
{
"localhost": {
"DAO": "0x65B0922fe7F0c4012aa38704071f26aeF6F22650",
"Work1": "0x95673D8710A8eD59f8551e9B12509D6812e0623e",
"Onboarding": "0xc6b3b8A641c52F7bC13a9D444e1f0759CA3b87b4",
"Proposals": "0x859cd550d5b3BDdde4Cf0ca71D060f945E9E42DD"
"DAO": "0x84A5F75A39e25bD39B69F7d096d159557EaF2a59",
"Work1": "0xaB3Bf8f9BE69289B0dd2a154a6390c8D9F780c59",
"Onboarding": "0xf10E261AFF9Aa8b05716002bFf44D3e990401C02",
"Proposals": "0xBD616B6331e0953Fc20281a54A684E614D8C4026"
},
"sepolia": {
"DAO": "0x8Cb4ab513A863ac29e855c85064ea53dec7dA24C",

View File

@ -1,9 +1,9 @@
{
"localhost": {
"DAO": "0x65B0922fe7F0c4012aa38704071f26aeF6F22650",
"Work1": "0x95673D8710A8eD59f8551e9B12509D6812e0623e",
"Onboarding": "0xc6b3b8A641c52F7bC13a9D444e1f0759CA3b87b4",
"Proposals": "0x859cd550d5b3BDdde4Cf0ca71D060f945E9E42DD"
"DAO": "0x84A5F75A39e25bD39B69F7d096d159557EaF2a59",
"Work1": "0xaB3Bf8f9BE69289B0dd2a154a6390c8D9F780c59",
"Onboarding": "0xf10E261AFF9Aa8b05716002bFf44D3e990401C02",
"Proposals": "0xBD616B6331e0953Fc20281a54A684E614D8C4026"
},
"sepolia": {
"DAO": "0x8Cb4ab513A863ac29e855c85064ea53dec7dA24C",

View File

@ -23,10 +23,11 @@ struct Stake {
}
struct ValidationPoolParams {
uint quorumPPB;
uint duration;
uint[2] quorum; // [ Numerator, Denominator ]
uint[2] winRatio; // [ Numerator, Denominator ]
uint bindingPercent;
bool redistributeLosingStakes;
uint[2] winRatio; // [ Numerator, Denominator ]
}
struct ValidationPool {
@ -37,7 +38,6 @@ struct ValidationPool {
uint stakeCount;
ValidationPoolParams params;
uint256 fee;
uint duration;
uint endTime;
bool resolved;
bool outcome;
@ -62,9 +62,9 @@ contract DAO is ERC20("Reputation", "REP") {
// TODO: possible parameter for winningRatio
// TODO: Add forum parameters
uint public constant minDuration = 1; // 1 second
uint public constant maxDuration = 365_000_000 days; // 1 million years
uint public constant minQuorumPPB = 100_000_000; // Parts per billion
uint constant minDuration = 1; // 1 second
uint constant maxDuration = 365_000_000 days; // 1 million years
uint[2] minQuorum = [1, 10];
event PostAdded(uint postIndex);
event ValidationPoolInitiated(uint poolIndex);
@ -74,6 +74,10 @@ contract DAO is ERC20("Reputation", "REP") {
bool quorumMet
);
function decimals() public pure override returns (uint8) {
return 9;
}
function addPost(
address author,
string calldata contentId
@ -103,7 +107,7 @@ contract DAO is ERC20("Reputation", "REP") {
require(duration >= minDuration, "Duration is too short");
require(duration <= maxDuration, "Duration is too long");
require(
(1_000_000_000 * quorum[0]) / quorum[1] >= minQuorumPPB,
minQuorum[1] * quorum[0] >= minQuorum[0] * quorum[1],
"Quorum is below minimum"
);
require(quorum[0] <= quorum[1], "Quorum is greater than one");
@ -116,11 +120,11 @@ contract DAO is ERC20("Reputation", "REP") {
pool.sender = msg.sender;
pool.postIndex = postIndex;
pool.fee = msg.value;
pool.params.quorumPPB = (1_000_000_000 * quorum[0]) / quorum[1];
pool.params.quorum = quorum;
pool.params.winRatio = winRatio;
pool.params.bindingPercent = bindingPercent;
pool.params.redistributeLosingStakes = redistributeLosingStakes;
pool.duration = duration;
pool.params.duration = duration;
pool.endTime = block.timestamp + duration;
pool.id = poolIndex;
pool.callbackOnValidate = callbackOnValidate;
@ -186,22 +190,39 @@ contract DAO is ERC20("Reputation", "REP") {
}
// Check that quorum is met
if (
1_000_000_000 * (stakedFor + stakedAgainst) <=
totalSupply() * pool.params.quorumPPB
pool.params.quorum[1] * (stakedFor + stakedAgainst) <=
totalSupply() * pool.params.quorum[0]
) {
// TODO: refund fee
// TODO: refund stakes
// Callback if requested
if (pool.callbackOnValidate) {
console.log("quorum not met, calling onValidate");
IOnValidate(pool.sender).onValidate(
votePasses,
false,
stakedFor,
stakedAgainst,
pool.callbackData
);
// Refund fee
// TODO: this could be made available for the sender to withdraw
// payable(pool.sender).transfer(pool.fee);
// Refund stakes
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
// TODO: ensure this can't be repeated
_update(address(this), s.sender, s.amount);
}
// Callback if requested
if (pool.callbackOnValidate) {
try
IOnValidate(pool.sender).onValidate(
votePasses,
false,
stakedFor,
stakedAgainst,
pool.callbackData
)
{
console.log("callbackOnValidate succeed");
} catch Error(string memory reason) {
console.log("callbackOnValidate failed:", reason);
}
}
pool.resolved = true;
emit ValidationPoolResolved(poolIndex, false, false);
return false;
}
@ -227,12 +248,12 @@ contract DAO is ERC20("Reputation", "REP") {
uint256 totalAllocated;
for (uint i = 0; i < pool.stakeCount; i++) {
s = pool.stakes[i];
bool redistributeLosingStakes = s.fromMint ||
pool.params.redistributeLosingStakes;
uint bindingPercent = s.fromMint ? 100 : pool.params.bindingPercent;
if (votePasses == s.inFavor) {
// Winning stake
// If this stake is from the minted fee, always redistribute it to the winners
bool redistributeLosingStakes = s.fromMint ||
pool.params.redistributeLosingStakes;
uint reward = redistributeLosingStakes
? ((s.amount * amountFromLosers) / amountFromWinners) *
(bindingPercent / 100)
@ -241,18 +262,20 @@ contract DAO is ERC20("Reputation", "REP") {
totalAllocated += reward;
} else {
// Losing stake
uint refund = (s.amount * (100 - bindingPercent)) / 100;
if (refund > 0) {
_update(address(this), s.sender, refund);
}
// If this stake is from the minted fee, don't burn it
if (!redistributeLosingStakes) {
uint amountToBurn = (s.amount *
pool.params.bindingPercent) / 100;
_burn(address(this), amountToBurn);
totalAllocated += amountToBurn;
if (!s.fromMint) {
uint refund = (s.amount * (100 - bindingPercent)) / 100;
if (refund > 0) {
_update(address(this), s.sender, refund);
}
if (!pool.params.redistributeLosingStakes) {
uint amountToBurn = (s.amount *
pool.params.bindingPercent) / 100;
_burn(address(this), amountToBurn);
totalAllocated += amountToBurn;
}
totalAllocated += refund;
}
totalAllocated += refund;
}
}
// Due to rounding, there may be some REP left over. Include this as a reward to the author.
@ -269,14 +292,19 @@ contract DAO is ERC20("Reputation", "REP") {
}
// Callback if requested
if (pool.callbackOnValidate) {
console.log("calling onValidate");
IOnValidate(pool.sender).onValidate(
votePasses,
true,
stakedFor,
stakedAgainst,
pool.callbackData
);
try
IOnValidate(pool.sender).onValidate(
votePasses,
true,
stakedFor,
stakedAgainst,
pool.callbackData
)
{
console.log("callbackOnValidate succeed");
} catch Error(string memory reason) {
console.log("callbackOnValidate failed:", reason);
}
}
}

View File

@ -18,14 +18,18 @@ contract Proposals is DAOContract, IOnValidate {
struct Pool {
uint poolIndex;
bool started;
bool completed;
uint stakedFor;
uint stakedAgainst;
bool votePasses;
bool quorumMet;
}
struct Referendum {
uint duration;
// Each referendum may retry up to 3x
Pool[] pools;
Pool[3] pools;
uint retryCount;
}
@ -45,12 +49,15 @@ contract Proposals is DAOContract, IOnValidate {
uint public proposalCount;
event NewProposal(uint proposalIndex);
event Attestation(uint proposalIndex);
event ReferendumStarted(uint proposalIndex, uint poolIndex);
event ProposalFailed(uint proposalIndex, string reason);
event ProposalAccepted(uint proposalIndex);
constructor(DAO dao) DAOContract(dao) {}
// TODO receive : we want to be able to accept refunds from validation pools
function propose(
string calldata contentId,
uint referendum0Duration,
@ -70,6 +77,15 @@ contract Proposals is DAOContract, IOnValidate {
emit NewProposal(proposalIndex);
}
function getPools(
uint proposalIndex
) public view returns (Pool[3][3] memory pools) {
Proposal storage proposal = proposals[proposalIndex];
pools[0] = proposal.referenda[0].pools;
pools[1] = proposal.referenda[1].pools;
pools[2] = proposal.referenda[2].pools;
}
/// External function for reputation holders to attest toward a given proposal;
/// This is non-binding and non-encumbering, so it does not transfer any reputation.
function attest(uint proposalIndex, uint amount) external {
@ -83,6 +99,7 @@ contract Proposals is DAOContract, IOnValidate {
proposal.attestationTotal -= proposal.attestations[msg.sender];
proposal.attestations[msg.sender] = amount;
proposal.attestationTotal += amount;
emit Attestation(proposalIndex);
}
// --- Sequences of validation pool parameters ---
@ -114,8 +131,10 @@ contract Proposals is DAOContract, IOnValidate {
true,
abi.encode(proposalIndex, referendumIndex, fee)
);
Pool storage pool = proposal.referenda[referendumIndex].pools.push();
Referendum storage referendum = proposal.referenda[referendumIndex];
Pool storage pool = referendum.pools[referendum.retryCount];
pool.poolIndex = poolIndex;
pool.started = true;
emit ReferendumStarted(proposalIndex, poolIndex);
}
@ -136,18 +155,22 @@ contract Proposals is DAOContract, IOnValidate {
(uint, uint, uint)
);
Proposal storage proposal = proposals[proposalIndex];
Referendum storage referendum = proposal.referenda[referendumIndex];
Pool storage pool = referendum.pools[referendum.retryCount];
// Make a record of this result
pool.completed = true;
pool.stakedFor = stakedFor;
pool.stakedAgainst = stakedAgainst;
pool.quorumMet = quorumMet;
pool.votePasses = votePasses;
if (!quorumMet) {
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Quorum not met");
proposal.remainingFee += fee;
// TODO: Refund remaining fee
return;
}
Referendum storage referendum = proposal.referenda[referendumIndex];
Pool storage pool = referendum.pools[referendum.pools.length - 1];
// Make a record of this result
pool.stakedFor = stakedFor;
pool.stakedAgainst = stakedAgainst;
// Participation threshold of 50%
bool participationAboveThreshold = 2 * (stakedFor + stakedAgainst) >=
@ -235,4 +258,16 @@ contract Proposals is DAOContract, IOnValidate {
initiateValidationPool(proposalIndex, 0, proposal.fee / 10);
return true;
}
/// External function to reclaim remaining fees after a proposal has completed all referenda
function reclaimRemainingFee(uint proposalIndex) external {
Proposal storage proposal = proposals[proposalIndex];
require(
proposal.stage == Stage.Failed || proposal.stage == Stage.Accepted,
"Remaining fees can only be reclaimed when proposal has been accepted or failed"
);
uint amount = proposal.remainingFee;
proposal.remainingFee = 0;
payable(msg.sender).transfer(amount);
}
}

View File

@ -117,7 +117,7 @@ describe('DAO', () => {
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);
expect(pool.params.duration).to.equal(POOL_DURATION);
expect(pool.postIndex).to.equal(0);
expect(pool.resolved).to.be.false;
expect(pool.sender).to.equal(account1);
@ -295,6 +295,25 @@ describe('DAO', () => {
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.totalSupply()).to.equal(295);
});
it('If redistributeLosingStakes is false and bindingPercent is 0, accounts should recover initial balances', async () => {
const init = () => initiateValidationPool({
bindingPercent: 0,
redistributeLosingStakes: false,
});
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
await dao.connect(account1).stake(2, 10, true);
await dao.connect(account2).stake(2, 10, false);
expect(await dao.balanceOf(account1)).to.equal(90);
expect(await dao.balanceOf(account2)).to.equal(90);
expect(await dao.balanceOf(dao.target)).to.equal(120);
time.increase(POOL_DURATION + 1);
await dao.evaluateOutcome(2);
expect(await dao.balanceOf(account1)).to.equal(200);
expect(await dao.balanceOf(account2)).to.equal(100);
expect(await dao.balanceOf(dao.target)).to.equal(0);
expect(await dao.totalSupply()).to.equal(300);
});
});
});
});

View File

@ -158,6 +158,14 @@ describe('Proposal', () => {
.to.emit(dao, 'ValidationPoolInitiated').withArgs(3);
proposal = await proposals.proposals(0);
expect(proposal.stage).to.equal(1);
const pools = await proposals.getPools(0);
expect(pools[0][0].started).to.be.true;
expect(pools[0][1].started).to.be.true;
expect(pools[0][2].started).to.be.false;
expect(pools[0][0].completed).to.be.true;
expect(pools[0][1].completed).to.be.false;
expect(pools[0][2].completed).to.be.false;
});
it('proposal fails if a referendum fails to meet participation rate 3 times', async () => {
@ -210,6 +218,11 @@ describe('Proposal', () => {
console.log('evaluated pool 2');
});
afterEach(async () => {
const pool = await dao.validationPools(3);
expect(pool.resolved).to.be.true;
});
it('proposal dies if it fails to meet quorum', async () => {
await time.increase(21);
await expect(dao.evaluateOutcome(3)).to.emit(dao, 'ValidationPoolResolved').withArgs(3, false, false);
@ -273,7 +286,7 @@ describe('Proposal', () => {
});
});
describe('Referendum 10% binding', () => {
describe('Referendum 100% binding', () => {
beforeEach(async () => {
await proposals.attest(0, 200);
await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2);
@ -291,6 +304,11 @@ describe('Proposal', () => {
expect(proposal.stage).to.equal(3);
});
afterEach(async () => {
const pool = await dao.validationPools(4);
expect(pool.resolved).to.be.true;
});
it('proposal dies if it fails to meet quorum', async () => {
await time.increase(21);
await expect(dao.evaluateOutcome(4)).to.emit(dao, 'ValidationPoolResolved').withArgs(4, false, false);