Technical Guide on building a dYdX Community Treasury Spending Proposal
A technical, step-by-step guide on how to create a proposal to transfer ethDYDX from the community treasury to a destination address.
Last updated
Reverie has put together a comprehensive, technical guide for submitting a governance proposal to transfer $ethDYDX from the Community Treasury through a Pull Request (PR) to the dYdX governance-contracts repository.
To create this proposal, a dYdX community member must have at least 5M Governance Tokens (0.5% of total supply) of proposal power (proposal threshold for a short timelock vote).
Preliminary Requirements
The following steps must be completed ahead of completing the Pull Request (PR):
Proposal Lifecycle: The DRC must be posted following the proposal template and there must be a successful Snapshot vote.
Destination Address: The destination address must be generated ahead of time. If the destination address is a multi-sig, the multi-sig wallet must be created.
GitHub account: A GitHub account to fork the repository.
Transfer Amount (Optional): Preferably, the requested transfer amount has been established ahead of the PR. However, if using a notional amount, it can be set as a final step prior to approval.
DIP IPFS Hash (Optional): If the transfer amount is known, the DIP should be finalized and pushed to IPFS to generate it’s hash. However, this can be set a final step prior to approval if the amount is not yet determined.
In src/config/index.ts, add two new variables to the configSchema constant that will be used for testing purposes. In the following code blocks, change the 'PROPOSAL_NAME' and 'PROPOSAL' fields to the name of the proposal being submitted.
Note: The funding amount will need to be multiplied by 10^18 per ERC20 standard. If the amount is not yet known, a temporary amount can be used (e.g. 10 → 10000000000000000000)
In src/lib/constants.ts, add the IPFS hash variable that will reference the DIP approved in the other repository:
src/lib/constants.ts...// Add a link to where the hash can be found exportconstDIP_NUMBER_IPFS_HASH='0x0000000000000000000000000000000000000000000000000000000000000000';
Note: If the DIP hasn’t been published yet, a temporary value can be used for testing (e.g. ‘0x0000000000000000000000000000000000000000000000000000000000000000’)
4. Proposal code
In src/migrations, create a new file named after the proposal → proposal-name.ts and populate with the following code:
b. Create the hardhat task and populate it with the proposal information on the task opening line.
Replace with the proposal-name in ‘deploy:proposal-name: and replace with a brief description in ‘Proposal Description’.
The last line calls the function you imported from the proposal code, so that will need to be adjusted.
tasks/deployment/proposal-name.tshardhatTask('deploy:proposal-name','Proposal Description.').addParam('proposalIpfsHashHex','IPFS hash for the uploaded DIP describing the proposal',DIP_NUMBER_IPFS_HASH,types.string).addParam('dydxTokenAddress','Address of the deployed DYDX token contract',mainnetAddresses.dydxToken,types.string).addParam('governorAddress','Address of the deployed DydxGovernor contract',mainnetAddresses.governor,types.string).addParam('shortTimelockAddress','Address of the deployed short timelock Executor contract',mainnetAddresses.shortTimelock,types.string).addParam('communityTreasuryAddress','Address of the deployed community treasury contract',mainnetAddresses.communityTreasury,types.string).setAction(async (args) => {awaitcreateProposalNameProposal(args); });
6. Building tests
Now that the code is ready for deployment, it’s time to build some tests around the proposal. Testing is done both locally and using a mainnet fork to simulate a proposal being executed on-chain.
a. Add Proposal Tests
In test/migrations, add a new file again with the proposal name → proposal-name.ts and include the following code:
Add the imports needed including the proposal functions:
createProposalNameProposal → this is the function we created in /src/migrations/proposal-name.
MOCK_PROPOSAL_IPFS_HASH → we’ll use a mock hash for testing purposes
Add the function to the getDeployedContractsForTest() function, outside of the last else loop:
test/helpers/get-deployed-contracts-for-test.tsasyncfunctiongetDeployedContractsForTest():Promise<AllDeployedContracts> {if (!config.isHardhat()) {returngetAllContracts(); }let deployedContracts:AllDeployedContracts;if (config.FORK_MAINNET) { deployedContracts =awaitgetAllContracts(); } else { deployedContracts =awaitdeployContractsForTest();// Execute the proposals which have already been executed on mainnet.//// The proposals will be executed when running on a local test network,// but will not be executed when running on a mainnet fork.awaitexecuteSafetyModuleRecoveryProposalsForTest(deployedContracts);awaitexecuteStarkProxyProposalForTest(deployedContracts);awaitexecuteGrantsProgramProposalForTest(deployedContracts);awaitexecuteGrantsProgramv15ProposalForTest(deployedContracts);awaitexecuteWindDownBorrowingPoolProposalForTest(deployedContracts);awaitexecuteUpdateMerkleDistributorRewardsParametersProposalForTest(deployedContracts);awaitexecuteWindDownSafetyModuleProposalForTest(deployedContracts); }awaitexecuteProposalNameProposalForTest(deployedContracts);// Execute the proposals which have not yet been executed on mainnet.awaitconfigureForTest(deployedContracts);return deployedContracts;}
d. Final Test file
Finally, we add a test of both the IPFS hash and the balance of the multisig after the mock proposal to ensure everything ends up as expected.
In test/misc, add a new file with the proposal name labelled → proposal-name-proposal.spec.ts and populate with these two tests:
We import the IPFS Hash from lib through DIP_NUMBER_IPFS_HASH
we hardcode the next proposalId number using ProposalNameId
we check the proposal hash with the constant Hash
we check the PROPOSAL_NAME_ADDRESS to see if it has an expected balance of the PROPOSAL_FUNDING_AMOUNT
Note: if this address already has DYDX, you will need to hardcode into the balance for the test to pass
test/misc/proposal-name-proposal.spec.tsimport { expect } from'chai';import { DIP_NUMBER_IPFS_HASH } from'../../src/lib/constants';import { describeContract, TestContext } from'../helpers/describe-contract';functioninit() {}describeContract('proposal-name', init, (ctx:TestContext) => {it('Proposal IPFS hash is correct',async () => {constProposalNameId= #;constproposal=awaitctx.governor.getProposalById(ProposalNameId);expect(proposal.ipfsHash).to.equal(DIP_NUMBER_IPFS_HASH); });it('Destination receives tokens from the community treasury',async () => {constbalance=awaitctx.dydxToken.balanceOf(ctx.config.PROPOSAL_NAME_ADDRESS);expect(balance).to.equal(ctx.config.PROPOSAL_FUNDING_AMOUNT); });});
7. Submitting the PR
Once all these code changes are made and saved locally, we can commit to the forked repository and open a PR to the dYdX repository for review: