CertiK Logo
CertiK Logo
Products
Company
incident-response
Back to all stories
Blogs
Security Considerations When Designing Blockchain Governance Systems
1/5/2023

Governance is one of the primary features of decentralized blockchain networks. Users come to consensus, or at least majority agreement, on the management of decentralized protocols. The unique features of blockchain governance systems offer many benefits, and a number of risks.

Before we take a look at some common security issues when dealing with blockchain governance systems, let’s go over how they function.

Security Considerations When Designing Blockchain Governance Systems

A blockchain governance system is a rule-based decision-making process. It allows the community to suggest, discuss and implement changes and improvements. In the blockchain world, there are two main types of governance: chain-level governance and application-level governance.

Chain-level governance is designed to manage and implement changes to the blockchain itself. In this type of governance, the rules for making changes are embedded in the blockchain protocols. Developers propose changes through code updates and each node votes on whether to accept or reject the proposed changes.

Application-level (or in the case of decentralized applications dApp-level) governance is similar to chain-level governance, but the scope is limited to application-level projects and the processing time is generally much shorter.

Screenshot 2022-12-29 at 9.20.12 AM

Below is the abstract of a simplified version of a dApp project. First, any users holding the governance token can create a proposal which starts in a reviewing period lasting two days. Within the two-day period the creator can cancel the proposal without any penalty. After, the proposal will then move to a 3-day voting period. During the voting period, governance token holders can execute a vote based on their voting power to vote “for/yes” or “against/no” on active proposals.

Voting power is usually determined by the amount of governance tokens held and affects the total weight of their vote. After the voting period, the governance system will start tallying vote results. The system will come up with a result of either “rejected” or “passed” with the pre-configured quorum and threshold. Finally, if the proposal is rejected, it will be canceled, and if it is passed, it will be executed by the system after timelock. The timelock helps inform users of upcoming changes.

Screenshot 2022-12-29 at 9.21.58 AM Source: Lucid

At the bottom of the diagram is a version with a deposit step and a “no-with-veto” voting option. It is slightly a more complicated solution adopted by some chains. A depositing period is like an extended version of the reviewing period. Each proposal has a minimum deposit field for tokens. Any users, including the proposer, can deposit to the proposal. If a proposal does not collect enough deposits within the two week depositing period, it will be canceled. The voting period is similar to the diagram’s top system but now with a longer period. The difference here is the voting options. The first system only includes “for/yes” and “against/no”, now two more options are introduced, which are “abstain” and ”no-with-veto“.

Voting “abstain” means the voting power does not have an opinion. So the abstentions are counted in the quorum check, but not in the threshold and veto checks. Therefore, at the end of tallying, besides “rejected” and “passed”, there will be an additional result called “rejected with veto”, which means the valid votes cast for “no-with-veto” exceed the veto check (e.g. 1/6). If a proposal results in “rejected-with-veto”, then there will be some implicit penalties. For instance, the deposit will not be refunded and potentially the deposit will be burned instead.

Terminology

Abstain - Abstain is a type of vote that is counted in Quorum but not in Threshold. It is uncommon in most protocols.

Deposit - Deposit is a mechanism that aims to avoid malicious users sabotaging the governance. A proposer (and community users interested in the proposal) needs to deposit a certain amount of tokens to a proposal during the deposit period. While certain proposals may require a token balance, this also validates a user's intention by putting their tokens at risk should the proposal fail. Once the proposal raises the determined amount of tokens, the proposal can move to the voting period.

Proposal - A proposal is a governance system component intended to enact a change in the system and be voted on by users. Users interact with proposals by creating a proposal, depositing a proposal, and voting for a proposal. Every user can submit a proposal. A proposal can be a parameter change, code upgrade change, distribution algorithm change, new feature request, etc.

Proposer - The proposer is the user who submitted the proposal. A proposer can – but does not have the responsibility to – provide full deposit funding.

Quorum - The quorum is the percentage of the total tokens that need to have voted at the end of the voting period.

Tally - Tally is a process to calculate the result of a proposal. The equation to determine if a proposal passes is: Pass = Quorum && Threshold (&& Veto). For example, for a protocol in that its quorum=40% and threshold=66.7%, a “pass” of a proposal requires that at least 40% of governance token holders participate in the proposal and that at least ⅔ of them voted “for/yes”.

Threshold - The threshold is the percentage of participating tokens that are required to vote "for/yes" to pass the proposal.

Veto - A veto is a type of vote that causes a proposal to fail when the percentage of vetoes exceeds a certain threshold. It can be seen as a stronger “against/no”. It is uncommon in most protocols. In some protocols, a “no-with-veto” result of a proposal may lead to some punishment.

Vote - Voting is how a governance participant (token holder) interacts with a proposal. Every participant can vote on a proposal during the voting period. A vote can be "for/yes" or "against/no", however some protocols also have voting options such as "abstain" and "no-with-veto".

On-Chain Versus Off-Chain Governance

An intuitive difference between on-chain and off-chain governance is the level of decentralization. Off-chain governance often depends on the decisions of the development or management organization. Admittedly, in the blockchain world, or more broadly in the open-source project world, off-chain governance can also be made more transparent through community meetings and public code reviews. Transparency does not equal decentralization. In many cases, the more numerous and less vocal community users do not actively and effectively participate in the governance. Changes to a blockchain or an application layer project are not made through a core development community that evaluates the pros and cons. Instead, each node is allowed to vote on the proposed changes and can read or discuss their benefits and drawbacks. It is decentralized because it relies on the community to make collective decisions.

Off-chain governance systems require time and effort among validators to reach consensus; while on-chain governance reaches consensus on proposed changes in relatively little time because of the rule-based decision-making feedback loop. Off-chain manipulation can lead to confusing situations where certain nodes can agree to disagree and not run proposed changes. The algorithm voting mechanism is relatively faster, as the results of testing its implementation can be seen through code updates.

Rule-based decision-making can automate and speed up the rate of change, but it cannot reduce conflicts. For example, if a group of community users insists that distribution algorithms must be modified to increase the liquidity and supply of their tokens, this may create inflation; while another group insists that the financial pain of less-liquid currency is necessary to ward off the evils of inflation. In these cases, in order to move forward with this for-profit project, there needs to be a person or a group that will step in and make a decision to break the unresolvable stranglehold - someone overriding the rules. This, of course, runs counter to the radically decentralized spirit of blockchain philosophy.

Though there are still barriers that guard the entrance of on-chain governance, these bars are generally lower in the on-chain world compared to off-chain. For almost all on-chain governance of application layer projects, the only bar is to become a governance token holder. Some projects have their governance tokens that can be purchased/swapped, while others can only be obtained through participating in the project. Majority of blockchain projects that require users to be KYC-ed in order to vote and participate in governance. Coupled with some voting rewards offered by community pools or rewarding distributions of some projects, on-chain voting is designed to greatly stimulate user participation and motivation.

Chain-Level Versus dApp-Level

For chain-level governance, votes by token holders are sometimes used in order to decide who operates the validator nodes that run a network (eg. Delegated Proof of Stake (DPoS) in EOS, Cosmos, etc.), sometimes to vote on protocol parameters (eg. the Ethereum gas limit) and sometimes to vote on and directly implement protocol upgrades wholesale (eg. Tezos). In all of these cases, the execution are automatic - the protocol itself contains all of the logic needed to change the validator set or to update its own rules, and does this automatically in response to the result of votes.[2]

Dapp-level governance has its idea derived from chain level governance. In the meantime, since Dapps are less complicated than a blockchain, the proposals cover more aspects. Some of the proposals are just like the chain-level proposals like reelecting the management committee or updating the parameters. Some of the proposals are not designed to be auto-triggered, like reimbursing expenses of adopting newly released features, establishing partnerships with other projects, or even setting up a joint venture project.

Common Security Issues

Lack of anti-flashloan mechanism

Some governance tokens can be flashloaned by any user. If there are no restraints on holding time when voting, a user can flashloan the governance token, create/vote for a malicious proposal and execute the proposal. This is the root cause of the Beanstalk exploit, which will dive into later.

Proposals missing validation period

To simplify the process, some projects choose to skip the reviewing/depositing period, which means that all of the proposals will move to the voting period no matter if the proposal is legitimate or not. This will increase the workload of users voting “no” to those malicious proposals. Or even worse, if the malicious proposal somehow passed, it would be executed.

Misconfiguration

Parameters in a governance system are sensitive and need to be set carefully. Some of the projects assigned those with inappropriate values, which may facilitate an attack. For instance, if the threshold of passing a proposal is too low, it is easier for an attacker to control the result of the proposal. If the timelock/delay period is too short, there would not be enough time for legitimate users to react if a malicious proposal is passed. They cannot mitigate this as the proposal would have been executed.

Wrongly Implemented Governance System

Incorrect system design and implementation can also result in drastic issues in a protocol. Unlike traditional tokens, the core functionality of the governance token is to vote on proposals. Some projects use their project token for governance, which allows the governance tokens to trade freely like a regular ERC 20 token. This will cause severe issues listed below:

  • Double voting by the same address
  • Votes are not cleared when voting power is removed
  • Delegated voting power is not removed by undelegate call

To mitigate the issues mentioned above, some projects require users to transfer tokens to the contract when voting, which will cause another common issue: Voting power cannot be reused for different proposals. In this scenario, a governance token can only be used for voting on one proposal at the same time. The issue here is that if there are multiple proposals in the voting period, it would be extremely difficult for some proposals to reach the threshold, causing some legitimate proposals to fail instead of execute.

Last but not least, we have seen in some systems, proposals can still be updated/voted after the tallying process. This will disrupt the workflow of the system, and have unpredictable consequences depending on the implementation of the system. For instance, if a proposal is never finalized, the deposit token will remain in the contract, when it is supposed to be returned to the depositor or burned, depending on the voting result of the proposal.

Case Study: Beanstalk Finance

Beanstalk is a “decentralized credit-based stablecoin protocol” that launched in 2021. Beanstalk’s primary objective is to incentivize independent market participants to regularly cross the price of 1 Bean over its US dollar peg in a sustainable fashion. Its governance mechanism consists of two different parts: BeanstalkDAO and the Stalk System.

BeanstalkDAO is the governing body of the protocol that proposes and votes on the execution of software upgrades. To join, users must deposit any whitelisted asset. Furthermore, there is an incentive to participate in the Silo to earn passive yields.

The Stalk System is the Silo’s financial incentive. When whitelisted assets are deposited into the Silo, Beanstalk rewards the depositor with Stalk and Seeds. Stalk is the governance token that allows users to participate in DAO votes and cast proposals. Seeds yield 1/10000 of a new Stalk every Season. Stalkholders are entitled to participate in Beanstalk governance and earn a portion of Bean mints. Governance power and distribution of Bean mints are proportional to each Stalkholder’s Stalk balance relative to total outstanding Stalk.

To kick off this particular exploit, the attacker funded their account, swapped tokens to BEAN tokens and deposited them to get Stalk, which gave them the ability to create and vote for proposals. They then created two proposals in two transactions, Beanstalk Improvement Proposal 18(BIP-18) and BIP-19.

BIP-18 was originally left blank, and BIP-19 contained a verified contract that proposed a $250k donation to the Ukraine wallet address, as well as $10k to the proposer. The proposal was used for transferring assets to the attacker and took 24 hours to proceed in order to invoke emergencyCommit().

Attack Flow

  1. The attacker flashloaned 350M Dai, 500M USDC, 150M USDT, 32M Bean and 11.6M LUSD
  2. The flashloaned assets are converted to 795,425,740 BEAN3Crv-f and 58,924,887 BEANLUSD-f:
  3. 1B (~ 350M Dai, 500M USDC, 150M USDT) were added to Curve.fi pool as liquidity, and received 979,691,328 DAI/USDC/USDT 3Crv tokens.
  4. 15M 3Crv in the above step is swapped for 15,251,318 LUSD and the remaining Crv are converted for 795,425,740 BEAN3Crv-f.
  5. 32,100,950 BEAB and 26,894,383 LUSD were added as liquidity and receive 58,924,887 BEANLUSD-f in return.
  6. The Attacker deposited all the gained assets from the flashloan in the Diamond contract and voted for the BIP18 proposal.
  7. The emergencyCommit() was immediately invoked to execute the BIP18 proposal.
  8. After the step 3 and 4, the attacker was able to drain the 36,084,584 BEAN, 0.54 UNIV2(BEAN-WETH), 874,663,982 BEAN3Crv and 60,562,844 BEANLUSD-f.
  9. The attacker used the drained assets (in Step5) to repay the flashloan and gain the rest as profit:
  10. 874,663,982 BEAN3Crv are removed from liquidity for 1,007,734,729 3Crv
  11. 60,562,844 BEANLUSD-f are removed from liquidity for 28,149,504 LUSD
  12. Repay 11,678,100 LUSD and 32,197,543 BEAN to corresponding pools
  13. 16,471,404 LUSD were swapped for 16,184,690 3Crv
  14. Burn all the 3Crv for 522,487,380 USDC, 365,758,059 DAI and 156,732,232 USDT
  15. Repay 350,315,000 DAI, 500,450,000 USDC and 150,135,000 to corresponding pools
  16. 0.54 UNIV2(BEAN-WETH) were removed from liquidity for 10,883 WETH and 32,511,085 BEAN
  17. 250,000 USDC were transferred to Ukraine Crypto Donation
  18. 15,443,059 DAI were swapped for 11,822 WETH and 37,228,637USDC were swapped for 2,124 WETH
  19. Finally, 24,830 WETH were transferred to the attacker.

But how does the attacker make the protocol transfering the token to himself? To answer this, we need to dive into the emergencyCommit() function.

emergencyCommit()

Normally, once a BIP is proposed, it requires a minimum of 7 days of voting time before being executed on-chain. This is supposed to act as a pseudo-timelock mechanism to allow proper time to verify the safety of the proposal. However, the emergencyCommit() function allows a proposal to be immediately executed on-chain following a waiting period of 1 day as opposed to 7, the threshold of emergencyCommit is ⅔.

The function emergencyCommit() allows people to “execute a specified bip as passed, create the associated diamond cut with the bip, pause the bip, and reward the proposer with uncompounded rewards” when a Stalk supermajority is reached.

Screenshot 2022-12-29 at 3.36.45 PM

A proposer that executes emergencyCommit() creates a diamond cut and can delegate to an address that will be _init() and have its logic executed. This allows proposers to execute anything they want.

Screenshot 2022-12-29 at 3.37.38 PM

After the proposal passed, The attacker created another contract, which contains the code to transfer the Silo’s deposited whitelisted assets to itself. Since Diamond performs an _init() on the contract(in the cutBip() function above), the underlying code has its functions executed via _calldata, and the attacker was able to drain around $76m worth of tokens.

Vulnerability

There are two issues that opened the door to the exploit. The first one is that the BEAN3Crv-f and BEANLUSD-f (used for voting) in the Silo system could be created via flashloan. Due to the lack of an anti-flashloan mechanism in the Beanstalk protocol, the attackers can borrow numerous tokens that are supported by the protocol and vote for malicious proposals.

The second issue is that the emergencycommit() function is overpowered. As mentioned above, when a proposal is passed, the governance system allows the proposer to do whatever he wants without any sort of validation. The emergencycommit() function allows the proposal to execute immediately, which does not leave any time to check the validity of the proposal.

The next two incidents do not directly exploit the vulnerability in the governance system, but the governance system plays a crucial role in the exploit.

Other Governance Exploits

Audius

The Audius governance contracts utilize the OpenZeppelin proxy upgradability pattern with an override to the standard implementation within the AudiusAdminUpgradabilityProxy contract. This permits proxy upgrades to the logic contracts of the Audius system.

In its implementation, the AudiusAdminUpgradabilityProxy uses storage slot 0 for the address of the proxyAdmin. The proxyAdmin for the Audius protocol was set to the governance system address of 0x4deca517d6817b6510798b7328f2314d3003abac. This caused a collision between the last two bytes in the proxyAdmin address and the two boolean state variables in OpenZeppelin's Initializable contract. With that being said, the last two bytes and the two booleans, “initialized” and “initializing”, are both stored in slot 0 (the first and second bytes). Given that the last byte of the proxyAdmin address is 0xac, because of the collision, initialized was interpreted as a truthy value. Similarly, because the second byte of the proxyAdmin address is 0xab, initializing was also interpreted as a truthy value. This enabled the initializer() modifier to always succeed:

require(initializing || isConstructor() || !initialized, "Contract instance has already been initialized"); 

The attacker was able to call the initializer method of deployed Audius contracts that implement Initializable and change storage state that is intended to be set only once in initialization. The attacker then submits a malicious proposal and steals 18M AUDIO tokens from the contract, which is worth 705 ETH at the time.

Fortress Protocol

Fortress is a lending protocol with an on-chain governance system. The governance contract can execute a successful proposal to modify the lending-related configuration (i.e., adding a collateral and its corresponding collateral factor). However, to successfully execute the proposal, the minimum FTS token that is required to vote is 400,000. As the price of the FTS token was low, the attacker only needed to swap ~11 ETH for over 400,000 FTS tokens by the time the attack happened. With over 400,000 FTS tokens, the attacker creates a malicious proposal and successfully executes it.

The other vulnerability is the “submit” function of the chain contract has a flaw that allows anyone to update the price.

What the attacker did here is chain these two issues together. They first flashloaned a huge amount of ETH and used them to purchase the FTS tokens for voting and collateral, then submitted and voted on a malicious proposal to change the collateral factor due to the low threshold, before updating the price of the FTS token in the oracle, and borrowing large amounts of other tokens from the contract, profiting approximately ~$3M.

Conclusion

Governance systems have evolved tremendously since the introduction of blockchain systems, but security challenges still remain. Governance systems are high-value targets for attackers, and projects should pay extra attention to their security during development. We suggest smart contract developers review and adopt mature open-source governance frameworks in order to avoid reinventing the wheel.