Dissecting the Elastic DAO protocol
Keeping the whales in check and giving the small apes a voice
There are a few existing problems with the current state of DAOs. First, most DAO members are inactive and only join a DAO as a free-rider. It is common to see proposals with low participation. Second, money can buy influence. People with money can buy more governance tokens and affect a DAO’s direction.
Elastic DAO has come to existence as an attempt to solve these problems. It is a governance protocol that seeks to enable token holders to take action against bad actors as well as give everyone a level playing field. Its key features are bonding curves, rebasing and fair governance.
There are 2 rules to the Elastic DAO:
When an Elastic DAO is in high demand, all current participants end up with more tokens via positive rebasing, and each token is worth more.
When work is happening inside the DAO, the value backing each token decreases.
When a user joins a DAO by minting EGT token using ETH as a collateral, the amount of ETH backing each EGT token increases. The smart contract then increases the supply of EGT and distributes them to EGT holders. Each EGT can also be redeemed for 3% more ETH if they decide to burn their EGT.
Each DAO has a treasury. When activities happen inside the DAO, people get rewarded for their work paid by the treasury. They have the option to sell the token in the secondary market or redeem for the underlying asset. This means each EGT is backed by less ETH, which makes an EGT’s value lower, thus it becomes cheaper for newcomers to join the DAO.
The Elastic DAO protocol tries to create a balance between decentralization and price speculation. In a DAO’s early days, not much work has been done and it is cheaper to get in. Once more work has been completed, the DAO’s value goes up and it becomes more expensive to join the DAO. It is almost like investing in a start-up. The earlier you invest in a start-up, the cheaper it is. That is because you are buying into a vision into the future. It becomes more expensive to invest in a later stage company because the foundation has been laid and it is a less risky investment.
A DAO member is free to exit the DAO any time by burning his EGT holdings and redeeming them for ETH.
Elastic DAO limits a user’s maximum influence and an earlier user holding more tokens does not translate to more opportunities and privileges. As of today, any EGT token holders can vote and anyone whose voting power exceeds the maximum voting power can submit proposals on snapshot. Snapshot allows users to vote by signing without submitting a transaction on-chain. After a proposal has been voted on, it is ratified by a 9 member multisig at elasticmultisig.eth
. The maximum voting power is a value set by the DAO at launch. It is not static as it changes along EGT rebases or it can be changed through proposals.
There is a rewarding/penalizing model to encourage DAO members to vote. An EGT holder gets rewarded for voting (approve, abstain or deny). The rewards will be distributed even if the proposal does not meet quorum (at least 50% voter turnout at least 60% approvals). The reward is equal to the number of EGT he votes with times 5%. The reward percentage is modifiable. On the other hand, if a proposal that was active for more than 24 hours doesn’t reach quorum, the proposer can request the multisig signers to penalize the people who didn’t vote. 10% (also modifiable) of a non-voter’s EGT will be taken away.
Creating a DAO
There are multiple steps involved in order to create a DAO. The initialization of a DAO is managed by the ElasticDAOFactory
’s deployDAOAndToken
function, which internally calls ElasticDAO
’s initialize.
Create the DAO (
ElasticDAO.sol
’sinitialize
)1a. Each DAO has at least 1 summoner.
require(_summoners.length > 0, 'ElasticDAO: At least 1 summoner required'); summoners = _summoners;
1b. There is an “ecosystem” for the DAO (It is a contract that stores the DAO’s core data).
/* Here is how an ecosystem looks like, * it follows the eternal storage pattern in Solidity. * In general, it keeps contract storage after a smart * contract upgrade. */ struct Instance { address daoAddress; address daoModelAddress; address ecosystemModelAddress; address tokenHolderModelAddress; address tokenModelAddress; address governanceTokenAddress; } Ecosystem.Instance memory defaults = Ecosystem(_ecosystemModelAddress).deserialize(address(0)); Ecosystem.Instance memory ecosystem = _buildEcosystem(controller, defaults);
1c. It creates the DAO.
DAO daoStorage = DAO(_ecosystem.daoModelAddress); DAO.Instance memory dao; dao.uuid = address(this); dao.ecosystem = _ecosystem; dao.maxVotingLambda = _maxVotingLambda; dao.name = _name; dao.summoned = false; dao.summoners = _summoners; /* * Serialization: It stores a DAO's value in a mapping, * where the key is the keccak256 hash of the concatenation * of the DAO's uuid and the property name. * * Deserialization: It gets the previously stored value * using the same key. */ daoStorage.serialize(dao);
Create the DAO’s governance token (
ElasticDAO.sol
’sinitializeToken
)2a. Only the deployer can do this and the DAO must not have been summoned.
function initializeToken( ) external onlyBeforeSummoning onlyDeployer nonReentrant { }
2b. It sets the parameters.
token.eByL = _eByL; /* initial ETH/token ratio */ token.ecosystem = _ecosystem; token.elasticity = _elasticity; /* The % by which capital delta should increase */ token.k = _k; /* Constant token multiplier */ token.lambda = 0; /* lambda is ElasticDAO's lingo for balance */ token.m = 1000000000000000000; /* Lambda modifier before rebase */ token.maxLambdaPurchase = _maxLambdaPurchase; /* Max amount of tokens one can purchase on each call */ token.name = _name; token.symbol = _symbol; token.uuid = _ecosystem.governanceTokenAddress;
A summoner can seed the DAO by transferring ETH to mint shares.
uint256 deltaE = msg.value; uint256 deltaLambda = ElasticMath.wdiv(deltaE, token.eByL); ElasticGovernanceToken(token.uuid).mintShares(msg.sender, deltaLambda);
The DAO can then be summoned. 🧞 Each summoner will get
_deltaLambda
number of shares.for (uint256 i = 0; i < dao.numberOfSummoners; i += 1) { tokenContract.mintShares(daoContract.getSummoner(dao, i), _deltaLambda); } dao.summoned = true;
These functions can only be called once and before the DAO is summoned.
Joining a DAO
A user can join a DAO by calling ElasticDAO.sol
’s join
function. It is a payable
function that accepts ETH and mints new DAO shares owned by the caller. The share price is based on a formula described here.
/*
* capital delta = each EGT's redemption value
* = total ETH balance in the DAO ÷ EGT total supply
*/
uint256 capitalDelta =
ElasticMath.capitalDelta(
SafeMath.sub(address(this).balance, msg.value), /* DAO ETH balance before user deposit */
tokenContract.totalSupply()
);
/*
* delta e = amount of ETH required to mint delta lambda (EGT amount)
*/
uint256 deltaE =
ElasticMath.deltaE(
token.maxLambdaPurchase,
capitalDelta,
token.k,
token.elasticity,
token.lambda,
token.m
);
/*
* token.maxLambdaPurchase = lambda delta
* lambda dash = the amount of lambda after the rebase
* m dash = the value of the lambda modifier after the rebase
*/
uint256 lambdaDash = SafeMath.add(token.maxLambdaPurchase, token.lambda);
uint256 mDash = ElasticMath.mDash(lambdaDash, token.lambda, token.m);
/*
* tokenContract = ElasticGovernanceToken.sol
* Inside mintShares, it updates the token holder's balance
* and the number of token holders if the token holder is not
* already a token holder based (based on its lambda value)
*/
bool success = tokenContract.mintShares(msg.sender, token.maxLambdaPurchase);
After minting shares to the user, it syncs each of its liquidity pools at Uniswap. (I am not exactly sure how this works, but I am assuming it has to do with the fact that shares have been minted and it can affect the DAO’s token’s liquidity pool in Uniswap? 🧐)
for (uint256 i = 0; i < liquidityPools.length; i += 1) {
IUniswapV2Pair(liquidityPools[i]).sync();
}
According to Uniswap’s whitepaper,
sync()
functions as a recovery mechanism in the case that a token asynchronously deflates the balance of a pair. In this case, trades will receive sub-optimal rates, and if no liquidity provider is willing to rectify the situation, the pair is stuck.sync()
exists to set the reserves of the contract to the current balances, providing a somewhat graceful recovery from this situation.
Since a user can only purchase up to a certain amount of EGT token, any ETH leftovers will be sent back to the user.
if (success && msg.value > deltaE) {
(success, ) = msg.sender.call{ value: SafeMath.sub(msg.value, deltaE) }('');
require(success, 'ElasticDAO: TransactionFailed');
}
Exiting a DAO
A user can join a DAO by calling ElasticDAO.sol
’s exit
function. The user decides how many EGT he wants to burn (delta lambda) to redeem the its underlying ETH. The amount of ETH to be transferred is equal to
/* (delta lambda ÷ lambda) x ETH in the DAO */
uint256 ratioOfShares = ElasticMath.wdiv(_deltaLambda, token.lambda);
uint256 ethToBeTransfered = ElasticMath.wmul(ratioOfShares, address(this).balance);
The DAO then burns the user’s shares and sends him the underlying ETH.
tokenContract.burnShares(msg.sender, _deltaLambda);
(bool success, ) = msg.sender.call{ value: ethToBeTransfered }('');
Rewarding voters
As voting currently happens in Snapshot, there is a need for the DAO’s controller to manually reward voters. According to ElasticDAO’s roadmap, voting is done off-chain to avoid high gas fees until they explore layer 2 solutions. To reward voters, the function reward
is called.
It can only be called by the controller.
modifier onlyController() {
require(msg.sender == controller, 'ElasticDAO: Only controller');
_;
}
It accepts two arguments, _addresses
and _amounts
, which are arrays of equal length.
/* The controller mints a specific number of shares for each voter */
for (uint256 i = 0; i < _addresses.length; i += 1) {
tokenContract.mintShares(_addresses[i], _amounts[i]);
}
Penalizing non-voters
Similarly, non-voters are penalized by having the controller to manually call a function (penalize
).
/* It burns _amounts[i] shares if it is more than the non-
* voter's current number of shares.
* Otherwise, it burns every share of the user and emit the
* event FailedToFullyPenalize.
*/
if (lambda < _amounts[i]) {
if (lambda != 0) {
tokenContract.burnShares(_addresses[i], lambda);
}
FailedToFullyPenalize(_addresses[i], _amounts[i], lambda);
} else {
tokenContract.burnShares(_addresses[i], _amounts[i]);
}
Limiting a user’s maximum voting power
ElasticDAO stores each DAO’s data in DAO.sol
‘s struct Instance
. A user’s maximum voting power is stored in the uint256 maxVotingLambda
. The controller can call the method setMaxVotingLambda
to set a voter maximum voting power in a DAO.
Ecosystem.Instance memory ecosystem = _getEcosystem();
DAO daoStorage = DAO(ecosystem.daoModelAddress);
DAO.Instance memory dao = daoStorage.deserialize(address(this), ecosystem);
dao.maxVotingLambda = _maxVotingLambda;
Each governance token holder has a number of shares and it is stored in the Ecosystem
through ElasticGovernanceToken
.
balanceOf
returns a token holder’s balance based onElasticMath
./* t = lambda * m * k */ function balanceOf(address _account) external view override returns (uint256) { Token.Instance memory token = _getToken(); TokenHolder.Instance memory tokenHolder = _getTokenHolder(_account); uint256 t = ElasticMath.t(tokenHolder.lambda, token.k, token.m); return t; }
balanceOfInShares
returns a token holder’s actual lambda balance.function balanceOfInShares(address _account) external view override returns (uint256) { TokenHolder.Instance memory tokenHolder = _getTokenHolder(_account); return tokenHolder.lambda; }
balanceOfVoting
returns a token holder’s voting power (ElasticMath
applied). It cannot exceed the DAO’smaxVotingLambda
.function balanceOfVoting(address _account) external view returns (uint256 balance) { Token.Instance memory token = _getToken(); TokenHolder.Instance memory tokenHolder = _getTokenHolder(_account); uint256 maxVotingLambda = _getDAO().maxVotingLambda; if (tokenHolder.lambda > maxVotingLambda) { return ElasticMath.t(maxVotingLambda, token.k, token.m); } else { return ElasticMath.t(tokenHolder.lambda, token.k, token.m); } }
For more information, there is a great Twitter thread by 0xRafi.