Implementing DAO Contracts in Yul (Solidity Assembly) with Voting, Withdrawal Functions, and Community Governance Best Practices
The term DAO (Decentralised Autonomous Organization) has roots dating back to the 90s, yet its understanding and application have evolved over time. Essentially, a DAO operates as a digital governance algorithm, where rules are encoded in 1s and 0s. However, the question arises: at what point does a digitised system truly become a DAO? This article explores this question in the context of the diverse landscape of decentralised, automated, and semi-automated communities.
As the popularity of DAOs continues to grow, the importance of establishing standards and comprehensive policies for DAO creators becomes evident. Many projects focus on governing their members without ensuring that the project itself follows the right path. Standardisation ensures greater adaptability and implementability, fostering interoperability among DAOs.
For a DAO to function effectively, it must embody three fundamental elements. The Three Fundamental 'C's of a DAO:
Recognising the potential for malicious actors to disrupt a community, the article introduces sub-nodes to distribute NFT holders. Each player is assigned a perpetual node number upon minting their first NFT. This approach ensures that players can only create votes and participate in voting within their designated node group, mitigating the risk of coordinated takeovers.
The article provides a detailed contract that encompasses voting, withdrawal, and the identification of the top 5 NFT holders. This implementation reflects a commitment to transparency, community engagement, and security in DAO governance.
// SPDX-License-Identifier: None
pragma solidity 0.8.0;
import {Check} from "https://github.com/aloycwl/Util.sol/blob/main/Security/Check.sol";
contract Node is Check {
event Transfer (address indexed, address indexed, uint);
event Vote (uint indexed, uint);
bytes32 constant private EVO = 0x33952ef907843fd2ddd118a92dd935debf65b72965a557a364ea08deffca032f;
function games (address adr) external view returns (bool stt, uint amt) {
assembly {
stt := sload(shl(0x05, adr))
amt := sload(shl(0x06, adr))
}
}
function checkVoting (uint ind) external view returns (uint, address, bytes32[5] memory, address) {
assembly {
mstore(0x00, ind)
let ptr := keccak256(0x00, 0x20)
for { let i } lt(i, 0x08) { i := add(i, 0x01) } {
mstore(add(0x80, mul(i, 0x20)), sload(add(ptr, i)))
}
return(0x80, 0x0100)
}
}
function count () external view returns (uint cnt) {
assembly { cnt := sload(INF) }
}
function topUp (address adr, uint amt) external {
assembly {
mstore(0x80, TFM)
mstore(0x84, caller())
mstore(0xa4, address())
mstore(0xc4, amt)
if iszero(call(gas(), sload(TTF), 0x00, 0x80, 0x64, 0x00, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER2)
revert(0x80, 0x64)
}
let tmp := shl(0x06, adr)
sstore(tmp, add(sload(tmp), amt))
}
}
function resourceOut (address adr, uint amt, uint8 v, bytes32 r, bytes32 s) external {
_transfer(msg.sender, amt);
isVRS(amt, v, r, s);
assembly {
if iszero(sload(shl(0x05, adr))) {
r := shl(0x06, adr)
s := sload(r)
if gt(amt, s) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER5)
revert(0x80, 0x64)
}
sstore(r, sub(s, amt))
}
}
}
function resourceIn (address adr, uint amt) external {
assembly {
if iszero(sload(shl(0x05, adr))) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
mstore(0x00, amt)
log3(0x00, 0x20, ETF, caller(), adr)
mstore(0x80, TFM)
mstore(0x84, caller())
mstore(0xa4, address())
mstore(0xc4, amt)
if iszero(call(gas(), sload(TTF), 0x00, 0x80, 0x64, 0x00, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER2)
revert(0x80, 0x64)
}
}
}
function createVote(address adr, uint stt) external returns(uint cnt) {
assembly {
mstore(0x00, TP5)
mstore(0x04, caller())
pop(staticcall(gas(), sload(TP5), 0x00, 0x24, 0x00, 0x40))
let nod := mload(0x20)
if iszero(nod) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
cnt := add(sload(INF), 0x01)
sstore(INF, cnt)
mstore(0x00, cnt)
let ptr := keccak256(0x00, 0x20)
sstore(ptr, stt)
sstore(add(ptr, 0x01), adr)
sstore(add(ptr, 0x07), caller())
sstore(add(ptr, 0x08), nod)
if gt(stt, 0x03) { stt := 0x03 }
mstore(0x00, stt)
log2(0x00, 0x20, EVO, cnt)
}
}
function cancelVote (uint cnt) external {
assembly {
mstore(0x00, cnt)
let ptr := keccak256(0x00, 0x20)
if iszero(eq(sload(add(ptr, 0x07)), caller())) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
sstore(ptr, 0x00)
sstore(add(ptr, 0x07), 0x00)
mstore(0x00, 0x00)
log2(0x00, 0x20, EVO, cnt)
}
}
function vote (uint ind, bool vot) external {
address toa;
uint amt;
assembly {
mstore(0x00, add(vot, 0x04))
log2(0x00, 0x20, EVO, ind)
mstore(0x00, ind)
let ptr := keccak256(0x00, 0x20)
let up
let down
mstore(0x00, TP5)
mstore(0x04, caller())
pop(staticcall(gas(), sload(TP5), 0x00, 0x24, 0x00, 0x40))
if or(iszero(mload(0x00)), iszero(eq(mload(0x20), sload(add(ptr, 0x08))))) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
let tmp
for { let i := add(ptr, 0x02) } lt(i, add(ptr, 0x07)) { i := add(i, 0x01) } {
let sli := sload(i)
if iszero(sli) {
tmp := i
break
}
if gt(sli, 0x00) {
switch gt(sli, STR)
case 1 {
up := add(up, 0x01)
mstore(0x00, sli)
mstore8(0x00, 0x00)
sli := mload(0x00)
}
default { down := add(down, 0x01) }
}
if eq(sli, caller()) {
tmp := 0x00
break
}
}
let sta := sload(ptr)
if or(iszero(sta), iszero(tmp)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
mstore(0x00, caller())
mstore8(0x00, vot)
sstore(tmp, mload(0x00))
if or(eq(up, 0x02), eq(down, 0x02)) {
switch vot
case 0x00 { down := add(down, 0x01) }
default { up := add(up, 0x01) }
mstore(0x00, 0x07)
if gt(up, 0x02) {
mstore(0x00, 0x06)
switch gt(sta, 0x02)
case 1 {
toa := sload(add(ptr, 0x01))
amt := sta
}
default {
sstore(shl(0x05, sload(add(ptr, 0x01))), mod(sta, 0x02))
}
}
sstore(ptr, 0x00)
log2(0x00, 0x20, EVO, ind)
}
}
if(amt > 0x00) _transfer(toa, amt);
}
function _transfer (address toa, uint amt) private {
assembly {
mstore(0x80, TTF)
mstore(0x84, toa)
mstore(0xa4, amt)
if iszero(call(gas(), sload(TTF), 0x00, 0x80, 0x44, 0x00, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER5)
revert(0x80, 0x64)
}
}
}
}
Below are the functions-by-functions explanation.
function games (address adr) external view returns (bool stt, uint amt) {
assembly {
stt := sload(shl(0x05, adr))
amt := sload(shl(0x06, adr))
}
}
By inputting the game owner’s address, it will return whether the game is accepted and the amount of token it has left. As mentioned earlier in this article, the normal route for a game to be onboarded in the DAO is through voting. If the game owner wants to input the game without voting, a quantity of the game token will have to be provided and the player will withdraw from the game pool instead of the node pool.
function checkVoting (uint ind) external view returns (uint, address, bytes32[5] memory, address) {
assembly {
mstore(0x00, ind)
let ptr := keccak256(0x00, 0x20)
for { let i } lt(i, 0x08) { i := add(i, 0x01) } {
mstore(add(0x80, mul(i, 0x20)), sload(add(ptr, i)))
}
return(0x80, 0x0100)
}
}
Every vote has a unique ID generated through a running number. When the ID is input, it will return the status of the vote in number, the address of the game or recipient, 5 arrays of bytes32 and the vote creator’s address. The 5 arrays of bytes32 is an interesting way to save on gas fee, since address is only 20 bytes therefore the 12 bytes before the address is wasted. If I were to store the vote status separately, it would consume 5 more storage slots.
An example on how to votes are stored:
bytes32: 0x0100000000000000000000005B38Da6a701c568545dCfcB03FcB875f56beddC4
As the first byte above is 0x01, it means the address 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 has voted for.
bytes32: 0x0000000000000000000000005B38Da6a701c568545dCfcB03FcB875f56beddC4
As the first byte above is 0x00, it means the address 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 has voted against
There are many ways to milk a cow, an additional field using binary is also possible to store the vote among many other methods. My objective is to provide a clear way to view the vote as a layman and also to easily extract the vote data programmatically.
As for the last field (vote creator’s address), this field is necessary to know who is the creator of this vote to know the person’s intention and for the person to cancel the vote before it ends.
function count () external view returns (uint cnt) {
assembly { cnt := sload(INF) }
}
This is a straightforward function, it returns the total number of votes.
function topUp (address adr, uint amt) external {
assembly {
mstore(0x80, TFM)
mstore(0x84, caller())
mstore(0xa4, address())
mstore(0xc4, amt)
if iszero(call(gas(), sload(TTF), 0x00, 0x80, 0x64, 0x00, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER2)
revert(0x80, 0x64)
}
let tmp := shl(0x06, adr)
sstore(tmp, add(sload(tmp), amt))
}
}
This function requires approval of the token before executing. It top ups in relation to the game owner’s address so the game does not need to be voted in and players can withdraw directly from the game’s pool.
function resourceOut (address adr, uint amt, uint8 v, bytes32 r, bytes32 s) external {
_transfer(msg.sender, amt);
isVRS(amt, v, r, s);
assembly {
if iszero(sload(shl(0x05, adr))) {
r := shl(0x06, adr)
s := sload(r)
if gt(amt, s) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER5)
revert(0x80, 0x64)
}
sstore(r, sub(s, amt))
}
}
}
This function requires the ecrecover() function which was explained in my previous post on ERC721 with Dynamic Payment . The DAO should already have tokens and it calls the direct transfer() function.
It detects if the game is not officially voted in, it not it will deduct the token from the game’s pool and only make the transfer if the pool have sufficient token left.
function resourceIn (address adr, uint amt) external {
assembly {
if iszero(sload(shl(0x05, adr))) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
mstore(0x00, amt)
log3(0x00, 0x20, ETF, caller(), adr)
mstore(0x80, TFM)
mstore(0x84, caller())
mstore(0xa4, address())
mstore(0xc4, amt)
if iszero(call(gas(), sload(TTF), 0x00, 0x80, 0x64, 0x00, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER2)
revert(0x80, 0x64)
}
}
}
This is for a player to transfer resources into the DAO to power up their game. Players can only transfer into a game that is approved and this function requires approval(). Upon success, 2 transfer events will be emitted. The first event will show the message sender transfer the tokens to this contract address, the other event will show the message sender transfer the tokens to the owner’s address. When these 2 events are detected, it signifies a resource in action and the game should reflect accordingly.
function createVote(address adr, uint stt) external returns(uint cnt) {
assembly {
mstore(0x00, TP5)
mstore(0x04, caller())
pop(staticcall(gas(), sload(TP5), 0x00, 0x24, 0x00, 0x40))
let nod := mload(0x20)
if iszero(nod) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
cnt := add(sload(INF), 0x01)
sstore(INF, cnt)
mstore(0x00, cnt)
let ptr := keccak256(0x00, 0x20)
sstore(ptr, stt)
sstore(add(ptr, 0x01), adr)
sstore(add(ptr, 0x07), caller())
sstore(add(ptr, 0x08), nod)
if gt(stt, 0x03) { stt := 0x03 }
mstore(0x00, stt)
log2(0x00, 0x20, EVO, cnt)
}
}
First it will detect if the message sender has a sub-node. A permanent sub-node address will be assigned to an address upon initial minting and even if the address no longer owns any NFT, it is still entitled to vote creation.
领英推荐
Next, the counter will increase by one and become the return value meaning this vote is tied to this number.
Finally, the vote type and the vote creator are saved in the storage and emit a vote created event.
function cancelVote (uint cnt) external {
assembly {
mstore(0x00, cnt)
let ptr := keccak256(0x00, 0x20)
if iszero(eq(sload(add(ptr, 0x07)), caller())) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
sstore(ptr, 0x00)
sstore(add(ptr, 0x07), 0x00)
mstore(0x00, 0x00)
log2(0x00, 0x20, EVO, cnt)
}
}
Only the creator is able to cancel the vote. Once the vote is cancelled, voters can no longer be able to vote on it.
function vote (uint ind, bool vot) external {
address toa;
uint amt;
assembly {
mstore(0x00, add(vot, 0x04))
log2(0x00, 0x20, EVO, ind)
mstore(0x00, ind)
let ptr := keccak256(0x00, 0x20)
let up
let down
mstore(0x00, TP5)
mstore(0x04, caller())
pop(staticcall(gas(), sload(TP5), 0x00, 0x24, 0x00, 0x40))
if or(iszero(mload(0x00)), iszero(eq(mload(0x20), sload(add(ptr, 0x08))))) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
let tmp
for { let i := add(ptr, 0x02) } lt(i, add(ptr, 0x07)) { i := add(i, 0x01) } {
let sli := sload(i)
if iszero(sli) {
tmp := i
break
}
if gt(sli, 0x00) {
switch gt(sli, STR)
case 1 {
up := add(up, 0x01)
mstore(0x00, sli)
mstore8(0x00, 0x00)
sli := mload(0x00)
}
default { down := add(down, 0x01) }
}
if eq(sli, caller()) {
tmp := 0x00
break
}
}
let sta := sload(ptr)
if or(iszero(sta), iszero(tmp)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
mstore(0x00, caller())
mstore8(0x00, vot)
sstore(tmp, mload(0x00))
if or(eq(up, 0x02), eq(down, 0x02)) {
switch vot
case 0x00 { down := add(down, 0x01) }
default { up := add(up, 0x01) }
mstore(0x00, 0x07)
if gt(up, 0x02) {
mstore(0x00, 0x06)
switch gt(sta, 0x02)
case 1 {
toa := sload(add(ptr, 0x01))
amt := sta
}
default {
sstore(shl(0x05, sload(add(ptr, 0x01))), mod(sta, 0x02))
}
}
sstore(ptr, 0x00)
log2(0x00, 0x20, EVO, ind)
}
}
if(amt > 0x00) _transfer(toa, amt);
}
This is lengthy so bear with me.
The first log2() event emits the voting decision of the voter, it is being called first to not conflict with the reuse of variables later on.
The “ptr” is the pointer of the vote’s storage and will fetch all its information.
If the voter does not belong to the top 5 from the same sub-node of the vote’s creator, it will revert an error message.
This function will then loop through all existing votes, start counting the for and against, and look for an empty slot.
The voter’s decision then is saved in the storage along with its address.
The last part will only be executed when there are either 3 for or against votes. Based on the nature of the vote, either game is added or removed, or the bulk withdrawal will take place.
The Top 5 contract.
// SPDX-License-Identifier: None
pragma solidity 0.8.0;
import {Hashes} from "https://github.com/aloycwl/Util.sol/blob/main/Hashes/Hashes.sol";
contract Top5 is Hashes {
function isTop5 (address adr) external view returns(bool bol, uint nod) {
assembly {
mstore(0x00, adr)
nod := sload(add(keccak256(0x00, 0x20), 0x01))
mstore(0x00, TP5)
mstore(0x20, nod)
let tmp := keccak256(0x00, 0x40)
for { let i } lt(i, 0x05) { i := add(i, 0x01) } {
if eq(adr, sload(add(tmp, i))) { bol := 0x01 }
}
}
}
function getTop5 (uint nod) external view returns(address[5] memory) {
assembly {
mstore(0x00, TP5)
mstore(0x20, nod)
let tmp := keccak256(0x00, 0x40)
mstore(0x80, sload(tmp))
mstore(0xa0, sload(add(tmp, 0x01)))
mstore(0xc0, sload(add(tmp, 0x02)))
mstore(0xe0, sload(add(tmp, 0x03)))
mstore(0x0100, sload(add(tmp, 0x04)))
return(0x80, 0xa0)
}
}
function _setTop5 (address top) internal {
uint nod = assignNode(top);
assembly {
mstore(0x00, TP5)
mstore(0x20, nod)
let tmp := keccak256(0x00, 0x40)
let ind
let lwt := ETF
for { let i } lt(i, 0x05) { i := add(i, 0x01) } {
let adr := sload(add(tmp, i))
if eq(adr, top) { return(0x00, 0x00) }
mstore(0x00, adr)
let ptr := sload(keccak256(0x00, 0x20))
if lt(ptr, lwt) {
ind := i
lwt := ptr
}
}
mstore(0x00, top)
if gt(sload(keccak256(0x00, 0x20)), lwt) {
sstore(add(tmp, ind), top)
}
}
}
function assignNode (address adr) internal returns (uint num) {
assembly {
mstore(0x00, adr)
let tmp := add(keccak256(0x00, 0x20), 0x01)
num := sload(tmp)
if iszero(num) {
mstore(0x00, timestamp())
mstore(0x20, caller())
num := add(mod(keccak256(0x00, 0x40), sload(ER5)), 0x01)
sstore(tmp, num)
}
}
}
}
This contract is to be imported by other contracts that need to set the top 5. It is separated from the DAO contract as it defines the governing parties. This is much shorter and as most of the functions already explained indirectly in this or other articles, below are the short explanations of each function
function isTop5 (address adr) external view returns(bool bol, uint nod) {
assembly {
mstore(0x00, adr)
nod := sload(add(keccak256(0x00, 0x20), 0x01))
mstore(0x00, TP5)
mstore(0x20, nod)
let tmp := keccak256(0x00, 0x40)
for { let i } lt(i, 0x05) { i := add(i, 0x01) } {
if eq(adr, sload(add(tmp, i))) { bol := 0x01 }
}
}
}
It fetches the sub-node of the address and loops through the sub-node’s top 5. It then return whether the address is top 5 and its sub-node number.
function getTop5 (uint nod) external view returns(address[5] memory) {
assembly {
mstore(0x00, TP5)
mstore(0x20, nod)
let tmp := keccak256(0x00, 0x40)
mstore(0x80, sload(tmp))
mstore(0xa0, sload(add(tmp, 0x01)))
mstore(0xc0, sload(add(tmp, 0x02)))
mstore(0xe0, sload(add(tmp, 0x03)))
mstore(0x0100, sload(add(tmp, 0x04)))
return(0x80, 0xa0)
}
}
Returns all the 5 addresses in the sub-node “nod”.
function _setTop5 (address top) internal {
uint nod = assignNode(top);
assembly {
mstore(0x00, TP5)
mstore(0x20, nod)
let tmp := keccak256(0x00, 0x40)
let ind
let lwt := ETF
for { let i } lt(i, 0x05) { i := add(i, 0x01) } {
let adr := sload(add(tmp, i))
if eq(adr, top) { return(0x00, 0x00) }
mstore(0x00, adr)
let ptr := sload(keccak256(0x00, 0x20))
if lt(ptr, lwt) {
ind := i
lwt := ptr
}
}
mstore(0x00, top)
if gt(sload(keccak256(0x00, 0x20)), lwt) {
sstore(add(tmp, ind), top)
}
}
}
Get the storage position of the sub-node and loop through all the 5 addresses. Call balanceOf() function to find the address with the lowest number of tokens. If the message sender has more tokens than the last person, replace the top 5 with message sender.
function assignNode (address adr) internal returns (uint num) {
assembly {
mstore(0x00, adr)
let tmp := add(keccak256(0x00, 0x20), 0x01)
num := sload(tmp)
if iszero(num) {
mstore(0x00, timestamp())
mstore(0x20, caller())
num := add(mod(keccak256(0x00, 0x40), sload(ER5)), 0x01)
sstore(tmp, num)
}
}
}
It checks the sub-node number of the address, if it does not exist, it will randomly create one for the message sender.
Set the ERC20 address in this DAO
bytes32 ptr = 0xa9059cbb00000000000000000000000000000000000000000000000000000000;
bytes32 b32;
assembly {
b32 := 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// Replace this with the ERC721 address
}
Proxy(payable(address(this))).mem(ptr, b32);
Set the ERC721 address in this DAO
bytes32 ptr = 0x2fea05d400000000000000000000000000000000000000000000000000000000;
bytes32 b32;
assembly {
b32 := 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// Replace this with the ERC20 address
}
Proxy(payable(address(this))).mem(ptr, b32);
Set the number of sub-nodes
bytes32 ptr = 0x00000007616d7420657272000000000000000000000000000000000000000000;
bytes32 b32;
assembly {
b32 := 10
// Replace this with the number of desired sub-node
}
Proxy(payable(address(this))).mem(ptr, b32);
The full repository is located here - DAO aloycwl Github