Like any other protocol, Nouns DAO relies on clients (webapps / mobile) to interact with its smart contracts. There are many reasons for the DAO to benefit from having multiple clients available, to name a few:
Redundancy of clients reduces risk of technical issues limiting access
The ability to sunset or stop developing nouns.wtf which is currently being maintained by Nounders
Gives space for new clients to emerge, including specialized clients like a proposal builder
Currently nouns.wtf is in an awkward place where it’s still probably the most popular client, but at the same time it’s not being actively maintained. It’s also not entirely clear how decisions are made on what can be added to the site.
We want to propose a model where clients get paid by the DAO based on usage. By usage we mean interactions with the smart contracts such as bidding on auctions, creating proposals, and voting. This is similar to the way Liquity incentivizes frontends with “kickbacks”.
We hope this will create the following incentives:
Incentivize clients to provide great UX (and do some minimum marketing) to drive more usage to their apps.
Incentivizes clients to solve for all the interactions the DAO pays for (and we can pay more for interactions with insufficient coverage).
Incentivizes clients to keep up with DAO upgrades, so they remain functional and continue to be used.
The simplest way this can work is by adding a client
parameter to the function calls, e.g.:
castVote(proposalId, support, reason, client)
where client
is the wallet address of the client that facilitated this transaction.
Let’s discuss the key interactions we think are worth paying for, how they could be paid, and how to make sure they can’t be gamed.
We don’t want to pay for every createBid
call because that can be easily gamed by making many low value bids. One way to get around it is to only pay for the interaction of the winning bid.
Fixed percentage of the auction settlement amount.
Fixed ETH amount.
Other options you think make sense? Please let us know
Having great proposal creation tools is important, supporting good previews, different transaction types, etc.
We don’t want to pay for every proposal creation because it may incentivize people to create spam proposals just to get the payment.
A few options on how to avoid it:
Only pay for successful proposals that get executed.
Only pay for proposals that pass quorum.
Let us know if you have more ideas.
Fixed ETH amount per proposal.
Fixed percentage of the amount of funding asked in the proposal. This is problematic because it creates weird incentives, and not all proposals are directly asking for funding.
Allocate a percentage of the DAO’s income to pay for proposals created, e.g. X% of auction revenue per month is used to pay governance clients for new valid proposals (valid meaning passed anti-gaming filters mentioned above).
Pay out disproportionately to top clients (e.g. 1st place gets 2x more than 2nd place).
In order to prevent spam proposals created just for voting payments, we can use the same method as mentioned above, i.e. only count votes made on “valid” proposals.
Fixed ETH amount per vote transaction.
Fixed ETH amount per vote, i.e. if voting with 2 nouns, client gets paid double.
Fixed ETH amount per proposal: e.g. if 1 ETH is paid out per proposal, and a proposal had 100 votes, then for each vote, the originating client gets 0.01 ETH.
We think the interactions listed above are the key interactions the DAO should pay for and those should be in the scope of the first iteration of this model.
Other interactions and possible payment options:
Proposal updates (updateProposal
)
Proposal candidates
All the interactions related to candidates have the goal of eventually turning the candidate into a Proposal via proposeBySigs
.
Payouts for proposal creation provide incentive for clients to support all the functionality of proposal candidates.
Assuming this is enough incentive, perhaps the specific candidate interactions (creating, updating, signing, feedback) don’t need payouts.
Delegation
Forking
Probably makes sense for fork-related interactions to be paid for by both OG DAO and the fork DAO.
Probably makes sense to pay per forking Noun, whether put in escrow or joining after fork DAO creation; this allows multiple clients to contribute to a single fork event and be fairly compensated.
How much should be paid per forking Noun? Should it be a nominal amount, e.g. 0.01 ETH? Or should it be a percentage of BV? Any other ideas?
A more general way to try to game the system, is to pretend to be a client by putting your own wallet address when interacting with the DAO contracts. This gets worse if people actually use clients, but swap the client address with their own when posting transactions.
One way to mitigate this risk is for the DAO to maintain a list of approved clients that can be paid. Whenever a new client wants to be added to this list, they can put up a proposal. It also serves as a way to advertise that this client exists.
Alternatively there could be some algorithmic way to determine that a client is likely not fake, e.g. condition payout on a minimum number of interactions, from different wallets, etc.
We’d like your feedback here. Do you think it’s better to keep a list of approved clients or rather take the permissionless route of codifying some rules to identify “real” clients? If the latter, what rules do you think will work well?
How much would the DAO need to pay to make it worthwhile for teams to develop clients?
We invite existing client teams to share some stats of their development and maintenance costs. Based on these numbers the DAO can pick some reasonable amounts to set as payouts.
We wanted to share some interim thoughts we had on how to implement this, and we welcome any feedback.
These are the requirements we had in mind so far:
Changing the amounts and formulas for payouts should not require an audit, i.e. should be outside the core DAO contracts
It should be possible to delegate the ability to change the payout scheme to a trusted multisig, e.g. Tech Grants Pod.
Small (ideally zero) additional gas cost for interacting with contracts.
In order to meet requirements (1) & (2), we imagine the payout being outside of the core DAO contracts. The DAO would explicitly fund this side contract via proposals.
The core DAO contracts would introduce a new client
parameter in the relevant functions, which would be either an ID or a wallet address to which to send payouts.
Regarding gas costs, we considered some methods that wouldn’t require writing to storage during the interactions:
Send a message to an L2 with the client id and interaction details; write it to storage on L2; payout calculation and disbursement on L2.
Use storage-proofs to provide evidence of the client ids to a contract on mainnet or an L2.
Both of these options add a certain degree of complexity and dependency on third parties.At the moment we’re leaning towards keeping everything on mainnet if it’s not too gas intensive.
For those interested, see a rough estimate for gas costs at the bottom of this post.
Please share your thoughts and feedback with us. Any ideas on better economics or technical solutions are very welcome.
We’re currently leaning towards implementing everything on mainnet for simplicity. Let us know if you think there’s a better way.
We want to hear your feedback before we move on to implementation:
Do you think our ideas to mitigate gaming are good? Should the DAO keep a list of approved clients? Do you have better ideas?
We plan to deploy everything on mainnet. Do you think we should use an L2 instead?
We want to focus on proposals, voting & auction bids at first. Do you think the V1 scope should be different?
What do you think would be the best payouts schemes?
How much would the DAO need to pay to make it worthwhile for teams to develop and maintain clients?
A rough estimate of the additional gas cost based on the pseudocode below:
AuctionHouse.createBid
One expensive SSTORE* per auction (setting a zeroed storage slot). By default that would be done by the first bidder, but we could also move it to be done by the settleCurrentAndCreateNewAuction
function.
Subsequent bids have minimal extra gas cost
DAO.propose
One expensive SSTORE per proposal to store the client address.
The current storage layout of proposals is not optimized so we can probably “squeeze-in” the client parameter without incurring extra gas cost.
DAO.castVote
One expensive SSTORE per client per proposal: the first vote cast from a client.
This can be reimbursed as part of vote refunds, so the cost is shifted to the DAO.
*expensive SSTORE: storage write of a non-zero value into a zero slot; about 20K gas, which at 100 gwei gas price and ETH at $1600 would be ~$3.2.
Below is some rough pseudocode of the changes required in the DAO’s core contracts to save the necessary data onchain:
createBid(uint nounId, address client) {
biddingClient[nounId] = client;
// regular bid code continues...
}
propose(targets, values, signatures, calldatas, description, address client) {
clientProposals[proposalId] = client;
// regular propose code continues...
}
castVoteWithReason(uint proposalId, uint support, string reason, address client) {
// record how many votes were cast per client, per proposal
clientVotes[proposalId][client].votes += votes;
// record how many voting txs were made per client, per proposal
clientVotes[proposalId][client].txs += 1;
// regular voting code continues...
}
Example pseudocode of a payout contract:
payoutForWinningBid(uint nounId) {
require(!payoutDone[nounId], ‘already paid’);
require(auctionHouse.auction().nounId > nounId, ‘auction not settled’);
payoutDone[nounId] = true;
// send fixed amount as payout
// to send a % of auction settlement price, we need to store the history of auction prices, which we plan to do in the next version of auction-house
sendETH(auctionHouse.biddingClient[nounId], PAYOUT_FOR_BIDDING);
}
payoutForProposal(uint proposalId) {
require(!payoutForProposalPaid[proposalId], ‘already paid’);
require(dao.state(proposalId) == EXECUTED);
payoutForProposalPaid[proposalId] = true;
// send fixed amount to the client
sendETH(dao.clientProposals[proposalId], PAYOUT_FOR_PROPOSAL);
}
payoutForVotes(uint proposalId, address client) {
require(!payoutForVotesPaid[proposalId], ‘already paid’);
require(state(proposalId) == EXECUTED);
payoutForVotesPaid[proposalId] = true;
Proposal proposal = dao.proposals(proposalId);
uint totalVotes = proposal.forVotes + proposal.againstVotes + proposal.abstainVotes;
sendETH(client, (dao.clientVotes[proposalId][client] / totalVotes) * PAYOUT_FOR_VOTES_PER_PROPOSAL);
}