implemented price change proposal workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 26s
Details
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 26s
Details
This commit is contained in:
parent
e3d194a7bd
commit
e3b6026d18
|
@ -15,7 +15,7 @@ import Stack from 'react-bootstrap/Stack';
|
|||
import './App.css';
|
||||
|
||||
import useList from './utils/List';
|
||||
import { getContractAddressByChainId, getContractNameByAddress } from './utils/contract-config';
|
||||
import { getContractAddressByChainId } from './utils/contract-config';
|
||||
import Web3Context from './contexts/Web3Context';
|
||||
import DAOArtifact from './assets/DAO.json';
|
||||
import Work1Artifact from './assets/Work1.json';
|
||||
|
@ -25,6 +25,7 @@ import AddPostModal from './components/posts/AddPostModal';
|
|||
import ViewPostModal from './components/posts/ViewPostModal';
|
||||
import Post from './utils/Post';
|
||||
import Proposals from './components/Proposals';
|
||||
import getAddressName from './utils/get-address-name';
|
||||
|
||||
function App() {
|
||||
const {
|
||||
|
@ -296,17 +297,6 @@ function App() {
|
|||
/* --------------------------- END UI ACTIONS ------------------------------------- */
|
||||
/* -------------------------------------------------------------------------------- */
|
||||
|
||||
const getAddressName = useCallback((address) => {
|
||||
const contractName = getContractNameByAddress(chainId, address);
|
||||
if (contractName) return `${contractName} Contract`;
|
||||
const addressParts = [
|
||||
address.slice(0, 7),
|
||||
|
||||
address.slice(address.length - 5),
|
||||
];
|
||||
return addressParts.join('...');
|
||||
}, [chainId]);
|
||||
|
||||
return (
|
||||
<Web3Context.Provider value={web3ProviderValue}>
|
||||
|
||||
|
@ -382,8 +372,8 @@ function App() {
|
|||
{posts.filter((x) => !!x).map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td>{post.id.toString()}</td>
|
||||
<td>{getAddressName(post.author)}</td>
|
||||
<td>{getAddressName(post.sender)}</td>
|
||||
<td>{getAddressName(chainId, post.author)}</td>
|
||||
<td>{getAddressName(chainId, post.sender)}</td>
|
||||
<td>
|
||||
<Button onClick={() => handleShowViewPost(post)}>
|
||||
View Post
|
||||
|
@ -392,15 +382,15 @@ function App() {
|
|||
Initiate Validation Pool
|
||||
{' '}
|
||||
<Button onClick={() => initiateValidationPool(post.id, 1)}>
|
||||
1 s
|
||||
1s
|
||||
</Button>
|
||||
{' '}
|
||||
<Button onClick={() => initiateValidationPool(post.id, 3600)}>
|
||||
1 Hr.
|
||||
<Button onClick={() => initiateValidationPool(post.id, 20)}>
|
||||
20s
|
||||
</Button>
|
||||
{' '}
|
||||
<Button onClick={() => initiateValidationPool(post.id, 86400)}>
|
||||
1 Day
|
||||
<Button onClick={() => initiateValidationPool(post.id, 60)}>
|
||||
60s
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -435,6 +425,7 @@ function App() {
|
|||
<br />
|
||||
Count
|
||||
</th>
|
||||
<th>Callback Ret Code</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
|
@ -444,7 +435,7 @@ function App() {
|
|||
<tr key={pool.id}>
|
||||
<td>{pool.id.toString()}</td>
|
||||
<td>{pool.postIndex.toString()}</td>
|
||||
<td>{getAddressName(pool.sender)}</td>
|
||||
<td>{getAddressName(chainId, pool.sender)}</td>
|
||||
<td>{pool.fee.toString()}</td>
|
||||
<td>
|
||||
{pool.params.bindingPercent.toString()}
|
||||
|
@ -456,6 +447,7 @@ function App() {
|
|||
<td>{pool.params.duration.toString()}</td>
|
||||
<td>{new Date(Number(pool.endTime) * 1000).toLocaleString()}</td>
|
||||
<td>{pool.stakeCount.toString()}</td>
|
||||
<td>{pool.onValidateRetCode.toString()}</td>
|
||||
<td>{pool.status}</td>
|
||||
<td>
|
||||
{!pool.resolved && reputation > 0 && pool.timeRemaining > 0 && (
|
||||
|
@ -464,9 +456,13 @@ function App() {
|
|||
Stake 1/2 REP
|
||||
</Button>
|
||||
{' '}
|
||||
<Button onClick={() => stake(pool.id, reputation, true)}>
|
||||
Stake All
|
||||
</Button>
|
||||
{' '}
|
||||
</>
|
||||
)}
|
||||
{!pool.resolved && pool.timeRemaining <= 0 && (
|
||||
{!pool.resolved && (pool.timeRemaining <= 0 || !reputation) && (
|
||||
<Button onClick={() => evaluateOutcome(pool.id)}>
|
||||
Evaluate Outcome
|
||||
</Button>
|
||||
|
|
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
|
@ -12,6 +12,7 @@ import { getContractAddressByChainId } from '../utils/contract-config';
|
|||
import AddPostModal from './posts/AddPostModal';
|
||||
import ViewPostModal from './posts/ViewPostModal';
|
||||
import Post from '../utils/Post';
|
||||
import getAddressName from '../utils/get-address-name';
|
||||
|
||||
const getProposalStatus = (proposal) => {
|
||||
switch (Number(proposal.stage)) {
|
||||
|
@ -107,9 +108,8 @@ function Proposals() {
|
|||
// TODO: Make referenda durations configurable
|
||||
await proposalsContract.current.methods.propose(
|
||||
post.hash,
|
||||
durations[0],
|
||||
durations[1],
|
||||
durations[2],
|
||||
account,
|
||||
durations,
|
||||
false,
|
||||
emptyCallbackData,
|
||||
).send({
|
||||
|
@ -141,7 +141,7 @@ function Proposals() {
|
|||
const pool = proposal.pools[referendumIndex][i];
|
||||
if (pool.started) {
|
||||
referenda.push(
|
||||
<div key={`${referendumIndex}.{i}`}>
|
||||
<div key={`${referendumIndex}.${i}`}>
|
||||
{`${referendumIndex}.${i}. `}
|
||||
{!pool.completed && (
|
||||
<span>
|
||||
|
@ -189,6 +189,7 @@ function Proposals() {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Sender</th>
|
||||
<th>Fee</th>
|
||||
<th>Stage</th>
|
||||
<th>Attestation</th>
|
||||
|
@ -200,6 +201,7 @@ function Proposals() {
|
|||
{proposals.filter((x) => !!x).map((proposal) => (
|
||||
<tr key={proposal.id}>
|
||||
<td>{proposal.id.toString()}</td>
|
||||
<td>{getAddressName(chainId, proposal.sender)}</td>
|
||||
<td>{proposal.fee.toString()}</td>
|
||||
<td>{getProposalStatus(proposal)}</td>
|
||||
<td>{proposal.attestationTotal.toString()}</td>
|
||||
|
|
|
@ -25,8 +25,17 @@ function PriceProposals() {
|
|||
|
||||
useEffect(() => {
|
||||
fetchPriceProposals();
|
||||
// TODO: Event subscriptions/unsubscriptions
|
||||
}, [workContract, fetchPriceProposals]);
|
||||
|
||||
const onPriceChangeProposed = (event) => {
|
||||
fetchPriceProposal(event.returnValues.priceProposalIndex);
|
||||
};
|
||||
|
||||
workContract.events.PriceChangeProposed({ fromBlock: 'latest' }).on('data', onPriceChangeProposed);
|
||||
|
||||
return () => {
|
||||
workContract.events.PriceChangeProposed().off(onPriceChangeProposed);
|
||||
};
|
||||
}, [workContract, fetchPriceProposals, fetchPriceProposal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -44,9 +44,6 @@ function ProposePriceChangeModal({
|
|||
setShow(false);
|
||||
// Write to API
|
||||
await post.write();
|
||||
// Publish to blockchain -- For now, Proposals.propose() does this for us
|
||||
// await post.publish(DAO, account);
|
||||
// Use content hash when calling Proposals.propose
|
||||
// TODO: Make durations configurable
|
||||
await workContract.methods.proposeNewPrice(proposedPrice, post.hash, [30, 30, 30]).send({
|
||||
from: account,
|
||||
|
|
|
@ -39,7 +39,20 @@ function WorkContract({
|
|||
}, [workContract, provider]);
|
||||
|
||||
useEffect(() => {
|
||||
const web3 = new Web3(provider);
|
||||
|
||||
fetchPrice();
|
||||
|
||||
const onPriceChangeAccepted = (event) => {
|
||||
setPriceWei(event.returnValues.price);
|
||||
setPriceEth(web3.utils.fromWei(event.returnValues.price, 'ether'));
|
||||
};
|
||||
|
||||
workContract.events.PriceChangeAccepted({ fromBlock: 'latest' }).on('data', onPriceChangeAccepted);
|
||||
|
||||
return () => {
|
||||
workContract.events.PriceChangeAccepted().off(onPriceChangeAccepted);
|
||||
};
|
||||
}, [workContract, provider, fetchPrice]);
|
||||
|
||||
const workContractProviderValue = useMemo(() => ({
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x358A07B26F4c556140872ecdB69c58e8807E7178",
|
||||
"Work1": "0xC62b0b16B3ef06c417BFC4Fb02E0Da06aF5A95Ef",
|
||||
"Onboarding": "0x91B8D37F396cfb887996119CD37a0886C78a7B9C",
|
||||
"Proposals": "0x63472674239ffb70618Fae043610917f2d9B781C"
|
||||
"DAO": "0x614fE39E47E48Ed39a078791917F6290C1C8D0cd",
|
||||
"Work1": "0x35d9024a19e970b1454Fa1C0e124dD2bd71E6360",
|
||||
"Onboarding": "0x861fD16fA5C26c53bf6C6E6210dD4d2364A26213",
|
||||
"Proposals": "0x918040581A6817fa2F3F6e73f7F10C3A3a7Bbc29"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0x58c8ea0ba031431423cD84787d7d57F0Bf7c6E63",
|
||||
"Work1": "0x67F6944504bF1b99fC6878AE7234A4dB6AB6dd1E",
|
||||
"Onboarding": "0xA44D0CAba4CB76e32966ea41dcECBc8A98347E68",
|
||||
"Proposals": "0x5A13A264214CDBfc949522dcCc48E5A0B11B3EdD"
|
||||
"DAO": "0x58c41E768aCA55B39b5dC0618c0D0bE3f5519943",
|
||||
"Work1": "0x6cEca2BB849c2a00786A05ed4fC64D08905724Cc",
|
||||
"Onboarding": "0x4b3906a6356F387bF5dd26FD34B072d20Cd40a7b",
|
||||
"Proposals": "0x3E1A6EE8D24Ba7D1392104B8652Bb0D2BDF127EE"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { getContractNameByAddress } from './contract-config';
|
||||
|
||||
const getAddressName = (chainId, address) => {
|
||||
const contractName = getContractNameByAddress(chainId, address);
|
||||
if (contractName) return `${contractName} Contract`;
|
||||
const addressParts = [
|
||||
address.slice(0, 7),
|
||||
|
||||
address.slice(address.length - 5),
|
||||
];
|
||||
return addressParts.join('...');
|
||||
};
|
||||
|
||||
export default getAddressName;
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x358A07B26F4c556140872ecdB69c58e8807E7178",
|
||||
"Work1": "0xC62b0b16B3ef06c417BFC4Fb02E0Da06aF5A95Ef",
|
||||
"Onboarding": "0x91B8D37F396cfb887996119CD37a0886C78a7B9C",
|
||||
"Proposals": "0x63472674239ffb70618Fae043610917f2d9B781C"
|
||||
"DAO": "0x614fE39E47E48Ed39a078791917F6290C1C8D0cd",
|
||||
"Work1": "0x35d9024a19e970b1454Fa1C0e124dD2bd71E6360",
|
||||
"Onboarding": "0x861fD16fA5C26c53bf6C6E6210dD4d2364A26213",
|
||||
"Proposals": "0x918040581A6817fa2F3F6e73f7F10C3A3a7Bbc29"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0x58c8ea0ba031431423cD84787d7d57F0Bf7c6E63",
|
||||
"Work1": "0x67F6944504bF1b99fC6878AE7234A4dB6AB6dd1E",
|
||||
"Onboarding": "0xA44D0CAba4CB76e32966ea41dcECBc8A98347E68",
|
||||
"Proposals": "0x5A13A264214CDBfc949522dcCc48E5A0B11B3EdD"
|
||||
"DAO": "0x58c41E768aCA55B39b5dC0618c0D0bE3f5519943",
|
||||
"Work1": "0x6cEca2BB849c2a00786A05ed4fC64D08905724Cc",
|
||||
"Onboarding": "0x4b3906a6356F387bF5dd26FD34B072d20Cd40a7b",
|
||||
"Proposals": "0x3E1A6EE8D24Ba7D1392104B8652Bb0D2BDF127EE"
|
||||
}
|
||||
}
|
|
@ -169,10 +169,6 @@ contract DAO is ERC20("Reputation", "REP") {
|
|||
function evaluateOutcome(uint poolIndex) public returns (bool votePasses) {
|
||||
ValidationPool storage pool = validationPools[poolIndex];
|
||||
Post storage post = posts[pool.postIndex];
|
||||
require(
|
||||
block.timestamp > pool.endTime,
|
||||
"Pool end time has not yet arrived"
|
||||
);
|
||||
require(pool.resolved == false, "Pool is already resolved");
|
||||
uint256 stakedFor;
|
||||
uint256 stakedAgainst;
|
||||
|
@ -185,6 +181,12 @@ contract DAO is ERC20("Reputation", "REP") {
|
|||
stakedAgainst += s.amount;
|
||||
}
|
||||
}
|
||||
// Special case for early evaluation if dao.totalSupply has been staked
|
||||
require(
|
||||
block.timestamp > pool.endTime ||
|
||||
stakedFor + stakedAgainst == totalSupply(),
|
||||
"Pool end time has not yet arrived"
|
||||
);
|
||||
// Check that quorum is met
|
||||
if (
|
||||
pool.params.quorum[1] * (stakedFor + stakedAgainst) <=
|
||||
|
@ -202,19 +204,13 @@ contract DAO is ERC20("Reputation", "REP") {
|
|||
emit ValidationPoolResolved(poolIndex, false, false);
|
||||
// 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);
|
||||
}
|
||||
IOnValidate(pool.sender).onValidate(
|
||||
votePasses,
|
||||
false,
|
||||
stakedFor,
|
||||
stakedAgainst,
|
||||
pool.callbackData
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -281,23 +277,18 @@ contract DAO is ERC20("Reputation", "REP") {
|
|||
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
|
||||
// TODO: Exclude encumbered reputation from totalSupply
|
||||
payable(member).transfer(share);
|
||||
}
|
||||
// Callback if requested
|
||||
if (pool.callbackOnValidate) {
|
||||
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);
|
||||
}
|
||||
IOnValidate(pool.sender).onValidate(
|
||||
votePasses,
|
||||
true,
|
||||
stakedFor,
|
||||
stakedAgainst,
|
||||
pool.callbackData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,5 +6,5 @@ interface IOnProposalAccepted {
|
|||
uint stakedFor,
|
||||
uint stakedAgainst,
|
||||
bytes calldata callbackData
|
||||
) external;
|
||||
) external returns (uint);
|
||||
}
|
||||
|
|
|
@ -8,5 +8,5 @@ interface IOnValidate {
|
|||
uint stakedFor,
|
||||
uint stakedAgainst,
|
||||
bytes calldata callbackData
|
||||
) external;
|
||||
) external returns (uint);
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ contract Onboarding is WorkContract, IOnValidate {
|
|||
uint,
|
||||
uint,
|
||||
bytes calldata callbackData
|
||||
) external {
|
||||
) external returns (uint) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"onValidate may only be called by the DAO contract"
|
||||
|
@ -61,7 +61,7 @@ contract Onboarding is WorkContract, IOnValidate {
|
|||
if (!votePasses || !quorumMet) {
|
||||
// refund the customer the remaining amount
|
||||
payable(request.customer).transfer(request.fee / 10);
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
uint postIndex = dao.addPost(
|
||||
request.customer,
|
||||
|
@ -77,5 +77,6 @@ contract Onboarding is WorkContract, IOnValidate {
|
|||
false,
|
||||
""
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ pragma solidity ^0.8.24;
|
|||
|
||||
import "./DAO.sol";
|
||||
import "./IOnValidate.sol";
|
||||
import "./IOnProposalAccepted.sol";
|
||||
|
||||
import "hardhat/console.sol";
|
||||
|
||||
|
@ -43,7 +44,7 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
mapping(address => uint) attestations;
|
||||
uint attestationTotal;
|
||||
Referendum[3] referenda;
|
||||
bool callbackOnValidate;
|
||||
bool callbackOnAccepted;
|
||||
bytes callbackData;
|
||||
}
|
||||
|
||||
|
@ -62,26 +63,26 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
|
||||
function propose(
|
||||
string calldata contentId,
|
||||
uint referendum0Duration,
|
||||
uint referendum1Duration,
|
||||
uint referendum100Duration,
|
||||
bool callbackOnValidate,
|
||||
address author,
|
||||
uint[3] calldata durations,
|
||||
bool callbackOnAccepted,
|
||||
bytes calldata callbackData
|
||||
) external payable returns (uint proposalIndex) {
|
||||
// TODO: Consider taking author as a parameter,
|
||||
// or else accepting a postIndex instead of contentId,
|
||||
// or support post lookup by contentId
|
||||
uint postIndex = dao.addPost(msg.sender, contentId);
|
||||
uint postIndex = dao.addPost(author, contentId);
|
||||
proposalIndex = proposalCount++;
|
||||
Proposal storage proposal = proposals[proposalIndex];
|
||||
proposal.sender = msg.sender;
|
||||
proposal.postIndex = postIndex;
|
||||
proposal.startTime = block.timestamp;
|
||||
proposal.referenda[0].duration = referendum0Duration;
|
||||
proposal.referenda[1].duration = referendum1Duration;
|
||||
proposal.referenda[2].duration = referendum100Duration;
|
||||
proposal.referenda[0].duration = durations[0];
|
||||
proposal.referenda[1].duration = durations[1];
|
||||
proposal.referenda[2].duration = durations[2];
|
||||
proposal.fee = msg.value;
|
||||
proposal.remainingFee = proposal.fee;
|
||||
proposal.callbackOnValidate = callbackOnValidate;
|
||||
proposal.callbackOnAccepted = callbackOnAccepted;
|
||||
proposal.callbackData = callbackData;
|
||||
emit NewProposal(proposalIndex);
|
||||
}
|
||||
|
@ -157,7 +158,7 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
uint stakedFor,
|
||||
uint stakedAgainst,
|
||||
bytes calldata callbackData
|
||||
) external {
|
||||
) external returns (uint) {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"onValidate may only be called by the DAO contract"
|
||||
|
@ -181,7 +182,7 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
proposal.stage = Stage.Failed;
|
||||
emit ProposalFailed(proposalIndex, "Quorum not met");
|
||||
proposal.remainingFee += fee;
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Participation threshold of 50%
|
||||
|
@ -214,33 +215,19 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
// Handle Referendum 100%
|
||||
} else if (proposal.stage == Stage.Referendum100) {
|
||||
require(referendumIndex == 2, "Stage 2 index mismatch");
|
||||
// Note that no retries are attempted for referendum 100%
|
||||
if (votePasses && participationAboveThreshold) {
|
||||
// The proposal has passed all referenda and should become "law"
|
||||
proposal.stage = Stage.Accepted;
|
||||
// This is an opportunity for some actions to occur
|
||||
// We should at least emit an event
|
||||
// Emit an event
|
||||
emit ProposalAccepted(proposalIndex);
|
||||
// We also execute a callback, if requested
|
||||
if (proposal.callbackOnValidate) {
|
||||
try
|
||||
// Note: We're directly reusing the onValidate hook we established for valdiation pools.
|
||||
// if any contracts want to use both callbacks, distinct interfaces should be defined.
|
||||
IOnValidate(proposal.sender).onValidate(
|
||||
votePasses,
|
||||
false,
|
||||
stakedFor,
|
||||
stakedAgainst,
|
||||
proposal.callbackData
|
||||
)
|
||||
{
|
||||
console.log("proposal callbackOnValidate succeed");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(
|
||||
"proposal callbackOnValidate failed:",
|
||||
reason
|
||||
);
|
||||
}
|
||||
// Execute a callback, if requested
|
||||
if (proposal.callbackOnAccepted) {
|
||||
IOnProposalAccepted(proposal.sender).onProposalAccepted(
|
||||
stakedFor,
|
||||
stakedAgainst,
|
||||
proposal.callbackData
|
||||
);
|
||||
}
|
||||
} else if (referendum.retryCount >= 2) {
|
||||
proposal.stage = Stage.Failed;
|
||||
|
@ -256,6 +243,7 @@ contract Proposals is DAOContract, IOnValidate {
|
|||
} else if (proposal.stage == Stage.Referendum100) {
|
||||
initiateValidationPool(proposalIndex, 2, proposal.fee / 10);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// External function that will advance a proposal to the referendum process
|
||||
|
|
|
@ -56,6 +56,8 @@ abstract contract WorkContract is
|
|||
event WorkAssigned(uint requestIndex, uint stakeIndex);
|
||||
event WorkEvidenceSubmitted(uint requestIndex);
|
||||
event WorkApprovalSubmitted(uint requestIndex, bool approval);
|
||||
event PriceChangeProposed(uint priceProposalIndex);
|
||||
event PriceChangeAccepted(uint priceProposalIndex, uint price);
|
||||
|
||||
constructor(
|
||||
DAO dao,
|
||||
|
@ -219,24 +221,25 @@ abstract contract WorkContract is
|
|||
value: msg.value
|
||||
}(
|
||||
contentId,
|
||||
durations[0],
|
||||
durations[1],
|
||||
durations[2],
|
||||
msg.sender,
|
||||
durations,
|
||||
true,
|
||||
abi.encode(priceProposalIndex)
|
||||
);
|
||||
emit PriceChangeProposed(priceProposalIndex);
|
||||
}
|
||||
|
||||
function onProposalAccepted(
|
||||
uint, // stakedFor
|
||||
uint, // stakedAgainst
|
||||
bytes calldata callbackData
|
||||
) external {
|
||||
) external returns (uint) {
|
||||
uint priceProposalIndex = abi.decode(callbackData, (uint));
|
||||
PriceProposal storage priceProposal = priceProposals[
|
||||
priceProposalIndex
|
||||
];
|
||||
price = priceProposal.price;
|
||||
// TODO: Emit price change event
|
||||
emit PriceChangeAccepted(priceProposalIndex, price);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,14 @@ const fetchPost = async (postIndex) => {
|
|||
const {
|
||||
id, sender, author, contentId,
|
||||
} = await dao.posts(postIndex);
|
||||
const { content } = await readFromApi(contentId);
|
||||
const { content, embeddedData } = await readFromApi(contentId);
|
||||
const post = {
|
||||
id,
|
||||
sender,
|
||||
author,
|
||||
contentId,
|
||||
content,
|
||||
embeddedData,
|
||||
};
|
||||
posts[postIndex] = post;
|
||||
return post;
|
||||
|
@ -78,7 +79,7 @@ const poolIsActive = (pool) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const poolIsValid = (pool) => {
|
||||
const poolIsValidWorkContract = (pool) => {
|
||||
switch (pool.sender) {
|
||||
case getContractAddressByNetworkName(network, 'Work1'): {
|
||||
// If this is a valid work evidence
|
||||
|
@ -93,11 +94,12 @@ const poolIsValid = (pool) => {
|
|||
return pool.post.content.startsWith(expectedContent);
|
||||
}
|
||||
default:
|
||||
console.log('Unrecognized sender %s', pool.sender);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const poolIsProposal = (pool) => pool.sender === getContractAddressByNetworkName(network, 'Proposals');
|
||||
|
||||
const getPoolStatus = (pool) => {
|
||||
if (poolIsActive(pool)) return 'Active';
|
||||
if (!pool.resolved) return 'Ready to Evaluate';
|
||||
|
@ -111,20 +113,34 @@ const stake = async (pool, amount, inFavor) => {
|
|||
await fetchReputation();
|
||||
};
|
||||
|
||||
const stakeEach = async (pools, amountPerPool) => {
|
||||
const conditionalStake = async (pool, amountToStake) => {
|
||||
if (poolIsValidWorkContract(pool)) {
|
||||
await stake(pool, amountToStake, true);
|
||||
} else if (poolIsProposal(pool)) {
|
||||
// We leave these alone at the moment.
|
||||
// We could consider automatic followup staking,
|
||||
// as a convenience if you decide early to favor a proposal
|
||||
} else {
|
||||
console.log('Unrecognized sender %s', pool.sender);
|
||||
await stake(pool, amountToStake, false);
|
||||
}
|
||||
};
|
||||
|
||||
const conditionalStakeEach = async (pools, amountPerPool) => {
|
||||
const promises = [];
|
||||
pools.forEach(async (pool) => {
|
||||
const inFavor = await poolIsValid(pool);
|
||||
promises.push(stake(pool, amountPerPool, inFavor));
|
||||
promises.push(conditionalStake(pool, amountPerPool));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const printPool = (pool) => {
|
||||
console.log(`pool ${pool.id.toString()}, `
|
||||
+ `status: ${getPoolStatus(pool)}, `
|
||||
+ `is valid: ${poolIsValid(pool)}, `
|
||||
+ `post content: ${pool.post?.content}`);
|
||||
const dataStr = pool.post?.embeddedData ? `data: ${JSON.stringify(pool.post.embeddedData)},\n ` : '';
|
||||
console.log(`pool ${pool.id.toString()}\n `
|
||||
+ `status: ${getPoolStatus(pool)},\n `
|
||||
+ `is valid work contract: ${poolIsValidWorkContract(pool)},\n `
|
||||
+ `is proposal: ${poolIsProposal(pool)},\n `
|
||||
+ `${dataStr}post content: ${pool.post?.content}`);
|
||||
};
|
||||
|
||||
async function main() {
|
||||
|
@ -136,7 +152,7 @@ async function main() {
|
|||
const activePools = validationPools.filter(poolIsActive);
|
||||
if (activePools.length && reputation > 0) {
|
||||
const amountPerPool = reputation / BigInt(2) / BigInt(activePools.length);
|
||||
await stakeEach(activePools, amountPerPool);
|
||||
await conditionalStakeEach(activePools, amountPerPool);
|
||||
}
|
||||
|
||||
// Listen for new validation pools
|
||||
|
@ -146,8 +162,7 @@ async function main() {
|
|||
await fetchReputation();
|
||||
if (!reputation) return;
|
||||
const amountToStake = reputation / BigInt(2);
|
||||
const inFavor = await poolIsValid(pool);
|
||||
await stake(pool, amountToStake, inFavor);
|
||||
await conditionalStake(pool, amountToStake);
|
||||
});
|
||||
|
||||
dao.on('ValidationPoolResolved', async (poolIndex, votePasses) => {
|
||||
|
|
|
@ -16,20 +16,35 @@ const readFromApi = async (hash) => {
|
|||
ca: readFileSync(caPath),
|
||||
});
|
||||
}
|
||||
const { data: { author, content, signature } } = await axios.get(`${apiUrl}/read/${hash}`, options);
|
||||
const {
|
||||
data: {
|
||||
author, content, signature, embeddedData,
|
||||
},
|
||||
} = await axios.get(`${apiUrl}/read/${hash}`, options);
|
||||
|
||||
// Verify hash
|
||||
const derivedHash = objectHash({ author, content, signature });
|
||||
const derivedHash = objectHash({
|
||||
author, content, signature, embeddedData,
|
||||
});
|
||||
if (derivedHash !== hash) {
|
||||
throw new Error('hash mismatch');
|
||||
}
|
||||
|
||||
// Verify embedded data
|
||||
let contentToVerify = content;
|
||||
if (embeddedData && Object.entries(embeddedData).length) {
|
||||
contentToVerify += `\n\n${JSON.stringify(embeddedData, null, 2)}`;
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
const account = recoverPersonalSignature({ data: content, signature });
|
||||
const account = recoverPersonalSignature({ data: contentToVerify, signature });
|
||||
if (account !== author) {
|
||||
throw new Error('author does not match signature');
|
||||
}
|
||||
return { author, content, signature };
|
||||
|
||||
return {
|
||||
author, content, signature, embeddedData,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = readFromApi;
|
||||
|
|
|
@ -167,8 +167,19 @@ describe('DAO', () => {
|
|||
});
|
||||
|
||||
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 not be able to evaluate outcome before duration has elapsed if not all rep has been staked', async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0));
|
||||
await initiateValidationPool({ fee: 100 });
|
||||
await expect(dao.evaluateOutcome(1)).to.be.revertedWith('Pool end time has not yet arrived');
|
||||
});
|
||||
|
||||
it('should not be able to evaluate outcome before duration has elapsed unless all rep has been staked', async () => {
|
||||
time.increase(POOL_DURATION + 1);
|
||||
await expect(dao.evaluateOutcome(0));
|
||||
await initiateValidationPool({ fee: 100 });
|
||||
await dao.stake(1, 100, true);
|
||||
await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true);
|
||||
});
|
||||
|
||||
it('should be able to evaluate outcome after duration has elapsed', async () => {
|
||||
|
|
|
@ -14,8 +14,10 @@ describe('Onboarding', () => {
|
|||
|
||||
const DAO = await ethers.getContractFactory('DAO');
|
||||
const dao = await DAO.deploy();
|
||||
const Proposals = await ethers.getContractFactory('Proposals');
|
||||
const proposals = await Proposals.deploy(dao.target);
|
||||
const Onboarding = await ethers.getContractFactory('Onboarding');
|
||||
const onboarding = await Onboarding.deploy(dao.target, PRICE);
|
||||
const onboarding = await Onboarding.deploy(dao.target, proposals.target, PRICE);
|
||||
|
||||
await dao.addPost(account1, 'content-id');
|
||||
const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
|
|
|
@ -79,7 +79,7 @@ describe('Proposal', () => {
|
|||
} = await loadFixture(deploy));
|
||||
|
||||
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
||||
await proposals.propose('proposal-content-id', 20, 20, 20, false, emptyCallbackData, { value: 100 });
|
||||
await proposals.propose('proposal-content-id', account1, [20, 20, 20], false, emptyCallbackData, { value: 100 });
|
||||
expect(await proposals.proposalCount()).to.equal(1);
|
||||
proposal = await proposals.proposals(0);
|
||||
expect(proposal.postIndex).to.equal(2);
|
||||
|
|
Loading…
Reference in New Issue