Navigating the DSS Governance Module: A Key to Decentralized Decision-Making in MakerDAO

The realm of decentralized finance (DeFi) is not only about innovative financial tools but also about novel governance structures. At the heart of MakerDAO, one of the pioneering platforms in DeFi, lies the DSS Governance Module. This article delves into its components and the unique governance process that empowers the MakerDAO community.

History

Maker governance was one of the first governance modules deployed on the Ethereum mainnet, back in 2017. It follows a very particular process not used by any other dapp (as far as the author knows).

Off-chain process (Polling)

There is a lengthy off chain process that starts with a discussion in the governance forum. Once the proposal reaches rough community consensus it is subjected to an off-chain governance vote. Results of governance votes are later registered on-chain in the Polling Emitter contract. If the governance vote passes, the proposed change is included in an executive vote, that will be the focus of this article.

Executive voting

Executive voting is performed on-chain, in the DS-Chief contract. Voting is different than most governances out there:

In the Chief, voters can choose to vote on any address (or a number of addresses, though this is not used currently). This address can be anything, including an EOA, though in practice it is always a standard contract called Spell. The address with the most votes is granted privileged access to schedule transactions in Pause, the Maker governance timelock. After the Pause delay, anyone can execute the scheduled Spell. Spells also contain a function that executes the previously scheduled transaction.

In the diagram below, we try to illustrate this:

image

Here Spell B has the most votes, and has the hat, meaning it has priviledge access to be "cast", or executed through the protocol timelock DS-Pause.

The community will then agree on a new spell, crafted by aggregating all actions agreed through governance. The new spell "C" is then deployed:

image

The community will then transfer votes from the previous spell to the new one:

image

And then anyone can "lift" the hat to the new spell ("C"), giving it the priviledged access it needs to be cast:

image

It differs from the usual governance not only on how proposals are voted. In this design, the previous passed proposal retains privileged access, until another one outnumbers it in votes. The community is incentivized to keep their MKR governance tokens staked into the Chief, or else the cost to attack the system lowers. With most of the supply of MKR staked, a second order effect of the low liquidity in the market is that it becomes even more expensive to attack the system.

Architecture

image
image

We will now delve into the smart contracts, and look at the key parts that enable the process we've just gone through.

Chief

Chief is the main governance contract. It sits on-chain as the Pause authority, conforming with the DS-Roles interface. The snippet below shows the key part that grants root access to the current hat:

	function isUserRoot(address who)
    	public view
    	returns (bool)
	{
    	  return (who == hat);
	}

The address on the state variable hat is granted root access to whatever contract the chief controls. The DSChief inherits from DSChiefApprovals, the contract that has the logic used for voting, locking MKR into the contract, and promoting an address to hat.

On chief approvals, a user journey will start by locking governance tokens into the contract:

	function lock(uint wad)
    	public
    	note
	{
          last[msg.sender] = block.number;
    	  GOV.pull(msg.sender, wad);
    	  IOU.mint(msg.sender, wad);
    	  deposits[msg.sender] = add(deposits[msg.sender], wad);
    	  addWeight(wad, votes[msg.sender]);
	}

	function free(uint wad)
    	public
    	note
	{
    	  require(block.number > last[msg.sender]);
    	  deposits[msg.sender] = sub(deposits[msg.sender], wad);
    	  subWeight(wad, votes[msg.sender]);
    	  IOU.burn(msg.sender, wad);
    	  GOV.push(msg.sender, wad);
	}

The functions lock and free are pretty self explanatory. They are used to lock and free governance tokens from the Chief contract. The deposits variable is updated with the current user's deposit, and governance tokens are moved. Voting weight is also updated, to the current slate the user previously voted for (an array of addresses).

Also note that a balance of the IOU token is minted to the user. The IOU is a non transferable token and it could be used to allow voting on other governances. It has never been used to date.

It also verifies when freeing governance tokens if it is not being performed in the block where the last deposit happened. This is actually a fix, before this, one could use a flashloan to vote for a slate. This happened once, when the B-Protocol team gave themselves privileged access to the next OSM Price. The proposal sat in DS-Pause for the 12 hours delay, then executed. The proposal was not malicious though, but the white hat nature of it pushed for a fix.

With tokens locked in the Chief, users can then vote:

	function vote(address[] memory yays) public returns (bytes32)
    	// note  both sub-calls note
	{
    	  bytes32 slate = etch(yays);
    	  vote(slate);
    	  return slate;
	}

	function vote(bytes32 slate)
    	public
    	note
	{
    	  require(slates[slate].length > 0 ||
        	slate == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, "ds-chief-invalid-slate");
    	  uint weight = deposits[msg.sender];
    	  subWeight(weight, votes[msg.sender]);
    	  votes[msg.sender] = slate;
    	  addWeight(weight, votes[msg.sender]);
	}

The functions above are where the voting logic is located. Both are public, but the first one will store a slate, while the second one, just verifies that a slate exists. The check was added as a fix to a bug found by OpenZeppelin.

The vote function will then proceed to move voting power to the user chosen slate. The hat variable was not touched here, so how does one promote the address with the most voting power to the `hat' variable?

	function lift(address whom)
    	public
    	note
	{
    	  require(approvals[whom] > approvals[hat]);
    	  hat = whom;
	}

The function lift is used for this purpose. Note that it can transfer the hat to any address with more votes than the current. This is not an issue as most votes are usually in the previous already executed Spell.

Pause

Pause is the contract that enforces a delay on all actions decided by governance. Once a transaction is scheduled, it will allow only governance to unschedule it. After the delay, it allows anyone to execute the transaction.

Main functions are:

	function plot(address usr, bytes32 tag, bytes memory fax, uint eta)
    	public note auth
	{
    	  require(eta >= add(now, delay), "ds-pause-delay-not-respected");
    	  plans[hash(usr, tag, fax, eta)] = true;
	}

	function drop(address usr, bytes32 tag, bytes memory fax, uint eta)
    	public note auth
	{
    	  plans[hash(usr, tag, fax, eta)] = false;
	}

	function exec(address usr, bytes32 tag, bytes memory fax, uint eta)
    	public note
    	returns (bytes memory out)
	{
    	  require(plans[hash(usr, tag, fax, eta)], "ds-pause-unplotted-plan");
    	  require(soul(usr) == tag,            	"ds-pause-wrong-codehash");
    	  require(now >= eta,                  	"ds-pause-premature-exec");

    	  plans[hash(usr, tag, fax, eta)] = false;

    	  out = proxy.exec(usr, fax);
    	  require(proxy.owner() == address(this), "ds-pause-illegal-storage-change");
	}

The current hat can call plot as many times as it wishes, but Spell contracts we will see next prevent this, allowing only a single schedule.

Proposal execution is made in the form of a delegatecall to a contract that contains all the logic. They are made through the pause proxy, a separate contract that owns all governed contracts in the system. DS-Pause was crafted this way so it's easy to check if the proposal tampered with the state of the contract. The proxy has only one state variable, that is checked on the last line of the execute function above, after the proposal has been executed.

contract DSPauseProxy {
	address public owner;
	modifier auth { require(msg.sender == owner, "ds-pause-proxy-unauthorized"); _; }
	constructor() public { owner = msg.sender; }

	function exec(address usr, bytes memory fax)
    	public auth
    	returns (bytes memory out)
	{
    	  bool ok;
    	  (ok, out) = usr.delegatecall(fax);
    	  require(ok, "ds-pause-delegatecall-error");
	}
}

Spell

The Spell is the actual executive proposal, it is crafted by one of the teams working on the protocol, and thoroughly reviewed by other teams. It contains all polls that were successful, and it will actually perform all changes once executed.

Spells are crafted/voted and executed on a biweekly basis.

They use a standard DssSpell contract. The spell also inherits dss-exec, a base contract for compliant spells, which, among other restrictions, can only be executed once.

The actual spell contract is just a DssExec instance, with hardcoded constructor params:

contract DssSpell is DssExec {
	constructor() DssExec(block.timestamp + 30 days, address(new DssSpellAction())) {}
}

The actions performed by the Spell are included in the DssSpellAction contract deployed in the Spell constructor. They are usually lengthy, so the snippet bellow contains just the beginning of the last spell from 2023:

	function actions() public override {
    	  // ---------- Stability Fee Changes ----------
    	  // Forum: https://forum.makerdao.com/t/stability-scope-parameter-changes-7/22882#increase-rwa014-a-coinbase-custody-debt-ceiling-9

    	  // Decrease the WBTC-A Stability Fee (SF) by 0.07%, from 5.86% to 5.79%
    	  DssExecLib.setIlkStabilityFee("WBTC-A", FIVE_PT_SEVEN_NINE_PCT_RATE, /* doDrip = */ true);

    	  // Decrease the WBTC-B Stability Fee (SF) by 0.07%, from 6.36% to 6.29%
    	  DssExecLib.setIlkStabilityFee("WBTC-B", SIX_PT_TWO_NINE_PCT_RATE, /* doDrip = */ true);

    	  // Decrease the WBTC-C Stability Fee (SF) by 0.07%, from 5.61% to 5.54%
    	  DssExecLib.setIlkStabilityFee("WBTC-C", FIVE_PT_FIVE_FOUR_PCT_RATE, /* doDrip = */ true);

    	  // ---------- Reduce PSM-GUSD-A Debt Ceiling ----------
    	  // Forum: https://forum.makerdao.com/t/stability-scope-parameter-changes-7/22882/2

    	  // Note: record currently set debt ceiling for PSM-GUSD-A
    	  (,,,uint256 lineReduction,) = vat.ilks("PSM-GUSD-A");

    	  // Remove PSM-GUSD-A from `Autoline`
    	  DssExecLib.removeIlkFromAutoLine("PSM-GUSD-A");
...

Above we can see the beginning of a spell, performing common actions such as stability fee and debt ceiling changes.

Here we completed the technical overview of the DSS Governance module, for more details on how it works, look at:

Who is Dewiz?

We enable DeFi innovators to build on top of the main DeFi protocols, providing technical expertise on both on-chain and off-chain integrations that can save 1000s of hours in engineering resources and put your project on the fast-track of the new financial world.

We are buidlers! Dewiz co-founders are former MakerDAO Core Unit contributors, with more than 2 year of hands-on experience with the Maker Protocol and and 5+ years of experience in the broader Ethereum ecosystem.

Our team has expertise in smart contract engineering for EVM chains, front-end engineering, on-chain/off-chain integrations and supporting services and agile project management.

If you are in need of builders for your project, reach out!

Article written by Oddaf (http://github.com/oddaf)

Subscribe to Dewiz
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.