Welcome to Aragon's veGovernance Plugin - a flexible, modular and secure system which can be used to create custom DAOs that foster a strong alignment between token holders and capital flows.
To get started, ensure that Foundry is installed on your computer.
Also make sure to install [GNU Make](https://www.gnu.org/software/make/).
# debian
sudo apt install build-essential
# arch
sudo pacman -S base-devel
# nix
nix-env -iA nixpkgs.gnumake
# macOS
brew install make
The Makefile
as the target launcher of the project. It's the recommended way to work with it. It manages the env variables of common tasks and executes only the steps that require being run.
The env.example file contains descriptions for all the initial settings. You don't need all of these right away but should review prior to fork tests and deployments
Fork testing has 2 modes:
-
"new-factory" will run against the live network fork, deploying new contracts via a new instance of the factory. See
make test-fork-testnet
,make test-fork-prodnet
and simmilar -
"existing-factory" will run against the live network fork, using the existing factory & therefore the existing contracts. See
make test-fork-factory-testnet
,make test-fork-factory-prodnet
and simmilar
In both cases, you will need to find the correct Aragon OSx contracts for the chain you wish to fork against. These can be found in the OSx commons repo
If running frequent fork tests it's recommended to pass a block number to enable caching
Deployments are done using the deployment factory. This is a singleton contract that will:
- Deploy all contracts
- Set permissions
- Transfer ownership to a freshly deployed multisig
- Store the addresses of the deployment in a single source of truth that can be queried at any time.
Check the available make targets to simulate and deploy the smart contracts:
- make pre-deploy-testnet Simulate a deployment to the defined testnet
- make pre-deploy-prodnet Simulate a deployment to the defined production network
- make deploy-testnet Deploy to the defined testnet network and verify
- make deploy-prodnet Deploy to the production network and verify
- I have cloned the official repository on my computer and I have checked out the corresponding branch
- I am using the latest official docker engine, running a Debian Linux (stable) image
- I have run
docker run --rm -it -v .:/deployment debian:bookworm-slim
- I have run
apt update && apt install -y make curl git vim neovim bc
- I have run
curl -L https://foundry.paradigm.xyz | bash
- I have run
source /root/.bashrc && foundryup
- I have run
cd /deployment
- I have run
make init
- I have printed the contents of
.env
and.env.test
on the screen
- I have run
- I am opening an editor on the
/deployment
folder, within the Docker container - The
.env
file contains the correct parameters for the deployment- I have created a brand new burner wallet with
cast wallet new
and copied the private key toDEPLOYMENT_PRIVATE_KEY
within.env
- I have reviewed the target network and RPC URL
- I have checked that the JSON file under
MULTISIG_MEMBERS_JSON_FILE_NAME
contains the correct list of signers - I have ensured all multisig members have undergone a proper security review and are aware of the security implications of being on said multisig
- I have checked that
MIN_APPROVALS
andMULTISIG_PROPOSAL_EXPIRATION_PERIOD
are correct - I have verified that
TOKEN1_ADDRESS
corresponds to an ERC20 contract on the target chain (same for TOKEN2 if applicable) - I have checked that
VE_TOKEN1_NAME
andVE_TOKEN1_SYMBOL
are correct (same for TOKEN2 if applicable) - I have checked that fee percent, warmup period, cooldown period, min lock duration, and min deposit:
- Have the expected values
- Cannot leave the voting contract or user tokens locked out
- I have checked that
VOTING_PAUSED
is true, should voting not be active right away - The multisig plugin repo and version:
- Correspond to the official contract on the target network
- Point to the latest stable release available
- The plugin ENS subdomain
- Contains a meaningful and unique value
- The given OSx addresses:
- Exist on the target network
- Contain the latest stable official version of the OSx DAO implementation, the Plugin Setup Processor and the Plugin Repo Factory
- I have verified the values on https://www.npmjs.com/package/@aragon/osx-commons-configs?activeTab=code >
/@aragon/osx-commons-configs/dist/deployments/json/
- I have created a brand new burner wallet with
- I have updated the
CurveConstantLib
andClock
with any new constants. - All my unit tests pass (
make test
) - Target test network
- I have defined
FORK_TESTNET_BLOCK_NUMBER
on.env.test
, with the current block number - I have run a fork test in
new-factory
mode with minted tokens against the official OSx contracts on the testnetmake test-fork-mint-testnet
- I have deployed my contracts successfully to the target testnet
make deploy-testnet
- I have updated
FACTORY_ADDRESS
on.env.test
with the address of the deployed factory - If there is a live token with an address holding ≥ 3000 tokens on the testnet:
- I have defined
TEST_TOKEN_WHALE
on.env.test
- I have run a fork test in
new-factory
mode with the live token on the testnetmake test-fork-testnet
- I have confirmed that tests still work in
existing-factory
mode with the live token(s) and the already deployed factory on the testnet.make test-fork-factory-testnet
- I have defined
- I have defined
- Target production network
- I have defined
FORK_PRODNET_BLOCK_NUMBER
on.env.test
, with the current block number - I have run a fork test in
new-factory
mode with minted tokens against the official OSx contracts on the prodnetmake test-fork-mint-prodnet
- If the live token has an address holding ≥ 3000 tokens on the prodnet:
- I have defined
TEST_TOKEN_WHALE
on.env.test
- I have updated
TOKEN1_ADDRESS
to have the address of the testnet token deployed above - I have run a fork test in
new-factory
mode with the live token on the prodnetmake test-fork-prodnet
- I have confirmed that tests still work in
existing-factory
mode with the live token(s) and the already deployed factory on the prodnet.make test-fork-factory-prodnet
- I have reverted
TOKEN1_ADDRESS
to the intended address of the token on the production network
- I have defined
- I have defined
- My deployment wallet is a newly created account, ready for safe production deploys.
- My computer:
- Is running in a safe physical location and a trusted network
- It exposes no services or ports
- The wifi or wired network used does does not have open ports to a WAN
- I have previewed my deploy without any errors
make pre-deploy-prodnet
- My wallet has sufficient native token for gas
- At least, 15% more than the estimated simulation
- Unit tests still run clean
- I have run
git status
and it reports no local changes - The current local git branch corresponds to its counterpart on
origin
- I confirm that the rest of members of the ceremony pulled the last commit of my branch and reported the same commit hash as my output for
git log -n 1
- I confirm that the rest of members of the ceremony pulled the last commit of my branch and reported the same commit hash as my output for
- I have initiated the production deployment with
make deploy-prodnet
- The deployment process completed with no errors
- The deployed factory was deployed by the deployment address
- The reported contracts have been created created by the newly deployed factory
- The smart contracts are correctly verified on Etherscan or the corresponding block explorer
- The output of the latest
deployment-*.log
file corresponds to the console output - I have transferred the remaining funds of the deployment wallet to the address that originally funded it
make refund
You can of course run all commands from the command line:
# Load the env vars
source .env
# run unit tests
forge test --no-match-path "test/fork/**/*.sol"
# Set the right RPC URL
RPC_URL="https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
# Run the deployment script
# If using Etherscan
forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify
# If using BlockScout
forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --verifier blockscout --verifier-url "https://sepolia.explorer.mode.network/api\?"
If you get the error Failed to get EIP-1559 fees, add --legacy
to the command:
forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verify --legacy
If some contracts fail to verify on Etherscan, retry with this command:
forge script --chain "$NETWORK" script/DeployGauges.s.sol:Deploy --rpc-url "$RPC_URL" --verify --legacy --private-key "$DEPLOYMENT_PRIVATE_KEY" --resume
The primary contracts in the governance hub are found in the src
directory. The key contracts include
VotingEscrowIncreasing.sol
: ERC721 veNFT designed to be used with escrow systems that reward users for longer lock times.GaugeVoter.sol
: allows split voting across arbitrary options. Votes are simply registered in the gauge voter, they do not perform any onchain actions
The main workflow in the Aragon VE Governance build is as follows:
- Users lock a whitelisted token into the Escrow Contract.
- The user is minted a veNFT (ERC721Enumerable) which stores:
- The amount they locked
- The start of their lock - users begin their locks starting from the current deposit interval
- In the base case, this means a user will start their lock from the start of the current week (Thursday 00:00 UTC)
- veNFT transfers are disabled by default, but can be enabled by governance.
- The user's voting power increases over time, starting from a baseline of the locked amount, up to a maximum voting power
- The user is unable to vote during an initial "warmup period".
- The user can exit their position at any time. In this case, they are entered into an "Exit Queue", whereupon their NFT is held in the queue for a "cooldown" period of X Days. After the period ends, they can burn the NFT to receieve their underlying balance back.
- It's possible to add a
minLock
period whereby a user is prevented from entering the exit queue before a certain time. This means they have their NFT available to vote but can't enter the exit process. - Voting power is removed from the NFT at this time
- The exit queue can optionally set an exit fee that will be charged on exit.
- It's possible to add a
- veNFTs can be consolidated into a single veNFT via merging or multiple sub-veNFTs can be created via splitting.
- Splitting can be done at any time, provided it has been enabled by the DAO
- Merging can only be done provided the veNFTs have the same start date or have reached maturity.
- Delegation is an option that can be enabled.
- Users can self delegate, or delegate to another address. Users can only delegate tokenIds to one address but not all tokenIDs need to be delegated.
- Delegation is exposed behind the
EscrowIVotesAdapter
which exposes an IVotes-compatible interface, this allows the escrow to be used in standard governance - Delegation dynamically adjusts with voting power, once a user delegates, the delegates total voting power will keep increasing until the user's veNFT reaches maturity.
- Delegation is updated on transfer, mint and burn.
-
Administrators setup voting options on the GaugeVoter, we call these
gauges
. -
Administrators can activate voting at which point a timestamp is recorded. By default there are 2 phases to a voting epoch:
- A Voting phase (default is 1 week), where votes are accepted.
- A distribution phase of (default is 1 week), where votes are not accepted (this is done in order to allow governance to compute and allocate rewards).
-
Users can vote as often as they want during the voting period, voting multiple times will calculate the latest voting power so it may be preferential to wait later in the period to maximise voting power.
-
Voting can be done using TokenIDs - see the
TokenGaugeVoter.sol
- or using Addresses - see theAddressGaugeVoter.sol
- Users vote by tokenID, votes by tokenId are tracked independently.
- Users' NFTs are locked unless they
reset
their votes and remove their voting power.
- The address gauge voter requires an IVotes compatible voting token
- Users must self delegate to vote on the voter
- Delegates can vote on the user's behalf without recieving approval to transfer the token
- The address voter exposes a hook that can be called to update voting power when delegate balances change.
- In our VE implementation, this automatically adjusts gauge votes when delegation changes
-
Various elements of these contracts can be parameterised in order to support different ve mechanisms. These include:
- Custom exit queue logic via custom exit queue managers
- Custom escrow curves
- Custom voting contracts other than SimpleGaugeVoter
- Custom epoch clock logic via the
Clock.sol
contract
-
Additionally, we use libraries like
CurveCoefficientLib
andSignedFixedPointMathLib
that allow users to make minimal, consistent and gas-efficient customisations to things like epoch length and curve shapes.
- The current versions of the contracts assume an offchain rewards distribution mechanism.
- Rewards are typically allocated in proportion to the voting power cast in the gauge.
- This version of the repository defines user-based logic and initial framework for:
- Rewards and emissions are assumed to be offchain
- delegateBySig is as-yet unsupported and will revert
- Only time-based clocks are supported in this version, support for block-based clocks (i.e. ERC20Votes w. block.number) is currently unsupported
To build a flexible approach to curve design, we reviewed implementations such as seen in Curve and Aerodrome and attempted to generalise Details on the curve design research can be found here
If upgrading from 1-0-0 or 1-1-0 to 1-2-0+, please note the behaviour changes with regards to new locks:
- Locks created pre upgrade will have start dates recorded at the end of the current weekly interval
- Locks created post upgrade will have start dates recorded at the start of the current weekly interval
The main risk vector here is the potential underflow concerns for 1-2-0+ contracts assuming lock.start >= block.timestamp.
While the contracts have been tested for this, we still recommend the following precautions:
- Set warmup periods to zero at least 1 deposit interval before the upgrade.
- Pause the contracts between the upgrade and the next deposit interval.
This ensures that all stakers who would be placed into a warmup period pre-upgrade have consistent behaviour post upgrade. This also ensures all stakers post upgrade have active locks - consistent with the expectations of the 1-2-0 contracts.