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: