Skip to content

Commit

Permalink
[Subgraph] ERC20 Extension Mapping (#315)
Browse files Browse the repository at this point in the history
* adding erc20 extension mappings

* updated test yaml and removing tokenBalance

* dont save reserved addresses as members

* lint fix readme

* token balance of the holder
  • Loading branch information
sophiacodes authored and fforbeck committed Sep 22, 2021
1 parent d486d9c commit 64d30c1
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 84 deletions.
43 changes: 35 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,27 @@

## Overview

At the LAO, we realized that even though Moloch is very useful and powerful, it has many features that are not necessary for all DAOs. There are also a few features that are missing and are difficult to add.
TributeDAO is a new modular, low cost DAO framework. The framework aims to improve DAOs by fixing the:

This is why we would like to introduce a more modular approach to Moloch architecture, which will give us:
- **Lack of modularity**: which has created challenges both in terms of extending, managing, and upgrading DAOs;
- **Rigid voting and governance mechanisms**: which limit the ability to experiment with additional forms of governance;
- **High costs**: especially for onchain voting;
- **Single token DAO structures**: which make it difficult to divide up economic and governance rights and create teams or sub-groups; and
- **Lack of NFT Support**: which makes it difficult for DAOs to be deployed for NFT projects.

- Simpler code - each module is responsible for only one function which reduces coupling and makes the system easier to understand.
- Adaptability - each part of the DAO can be adapted to the needs of a particular DAO without the need to audit the entire code base every time.
- Upgradability - modules can be easily upgraded as necessary. For example, as the voting process evolves over time the module responsible for managing the voting process can be upgraded without changing any other modules or the Core Contract. Modules can also be used by multiple DAOs without the need to be redeployed.
The TributeDAO framework aims to address these issues, as part of our quest to make DAOs the dominant form of organization. As the growing number of participants in DAOs know, there is no “one size fits all” for managing any organization. DAOs need low cost and easy to develop components that can be assembled like lego blocks to fit the needs of the organization and its membership.

## Proposed Evolution of MolochDAO Framework

The TributeDAO framework is our team's tribute to the MolochDAO ecosysten. As many know, MolochDAO brought new life to DAOs. Through an elegant smart contract design, this smart contract framework brought DAOs back to life, helping us push beyond the fiery depths of “The DAO.”

Last year, we worked to evolve the initial MolochDAO smart contracts by assisting with the creation of Moloch v2, which enabled multiple token support, “guildkicks” to remove unwanted members, and “loot” to issue non-voting shares still entitled to financial distributions. These upgraded contracts were built with “venture” and similar investment transactions in mind, allowing for more effective swaps and control over tokenized assets and membership.

The TributeDAO framework hopes to provide teams looking to deploy DAOs with several enhancements and improvements, including:

- **Simpler code** - each module is responsible for only one function which reduces coupling and makes the system easier to understand.
- **Adaptability** - each part of the DAO can be adapted to the needs of a particular DAO without the need to audit the entire code base every time.
- **Upgradability** - modules can be easily upgraded as necessary. For example, as the voting process evolves over time the module responsible for managing the voting process can be upgraded without changing any other modules or the Core Contract. Modules can also be used by multiple DAOs without the need to be redeployed.

Inspired by the [hexagonal architecture design pattern](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>) we believe that we can have additional layers of security, and break the main contract into smaller contracts. With that, we create loosely coupled modules/contracts, easier to audit, and can be easily connected to the DAO.

Expand All @@ -27,15 +41,15 @@ The main design goal is to limit access to the smart contracts according at laye

There are five main components in the Tribute architecture:

#### External World
The core contracts serve as the spine for the Tribute DAO framework and act as a DAO registry, creating a digital version of "division of corporations." These contracts compose the DAO itself, and make it cheaper and easier to deploy a DAO. These contracts directly change the DAO state without the need of going through an adapter or extension (described further below). A core contract never pulls information directly from the external world. For that we use Adapters and Extensions, and the natural information flow is always from the external world to the core contracts.

The external world is essentially anything that interacts with the DAO. An example of that are RPC clients that are responsible for calling the Adapters public/external functions to pull/push data to the DAO Core Contracts and its Extensions.

#### Adapters

Adapters are well-defined, tested and extensible smart contracts that are created with a unique purpose. One Adapter is responsible for performing one or a set of tasks in a given context. With this approach we can develop adapters targeting specific use-cases, and update the DAO configurations to use these new adapters.

When a new adapter is created, one needs to submit a Managing proposal to add the new adapter to the DAO. Once the proposal passes, the new adapter is added and becomes available for use.
Once a DAO is created using the above core contracts, they can be extended and modified with adapters and extensions. Adapters and extensions make it easy to assemble a DAO like lego blocks, by adding to a DAO narrowly-defined, tested, and extensible smart contracts created for specific purposes. Adapters and extensions make DAOs more modular, upgradeable, and also enable us to work together to build robust DAO tooling. They can be added to a TributeDAO via a DAO vote.

Each adapter needs to be configured with the [Access Flags](#access-control-layer) in order to access the [Core Contracts](#core-contracts), and/or [Extensions](#extensions). Otherwise the Adapter will not able to pull/push information to/from the DAO.

Expand All @@ -54,7 +68,20 @@ Adapters implemented in the Tribute project:
- [Voting](https://github.com/openlawteam/tribute-contracts/blob/master/docs/adapters/Voting.md): adds the simple on chain voting governance process to the DAO.
- [Withdraw](https://github.com/openlawteam/tribute-contracts/blob/master/docs/adapters/Withdraw.md): allows the members to withdraw their funds from the DAO bank.

Considerations:
The range of potential adapters will expand over time and likely will include:

- "Streams" to manage a DAO's treasury in a more agile way
- Alternative voting structures to layer to improve DAO governance, including quadratic voting, one-member-one-vote voting
- Swaps of one token for another
- Streaming payments
- NFT-based onboarding
- DAO-to-DAO voting
- Creating a liquidity pool for a DAO's native asset
- Staking or depositing assets into existing DeFi projects (like Aave, Compound, or Lido)

Creating an adapter is straight forward and should save developers engineering time. Each adapter needs to be configured with the [Access Flags](#access-control-layer) in order to access the [Core Contracts](#core-contracts), and/or [Extensions](#extensions). Otherwise the Adapter will not able to pull/push information to/from the DAO.

Please note:

- Adapters do not keep track of the state of the DAO. An adapter might use storage to control its own state, but ideally any DAO state change must be propagated to the DAORegistry Core Contract.
- Adapters just execute smart contract logic that changes the state of the DAO by calling the DAORegistry. They also can compose complex calls that interact with External World, other Adapters or even Extensions, to pull/push additional information.
Expand Down
145 changes: 86 additions & 59 deletions subgraph/mappings/extensions/bank-extension-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ import {
NewBalance,
Withdraw,
} from "../../generated/templates/BankExtension/BankExtension";
import { ERC20 } from "../../generated/templates/BankExtension/ERC20";
import { ERC20Extension } from "../../generated/templates/BankExtension/ERC20Extension";
import {
Member,
TributeDao,
Token,
TokenBalance,
TokenHolder,
Extension,
} from "../../generated/schema";
import { GUILD, UNITS, TOTAL } from "../helpers/constants";
import {
GUILD,
UNITS,
TOTAL,
MEMBER_COUNT,
ERC20_EXTENSION_ID,
} from "../helpers/constants";

function internalTransfer(
createdAt: string,
Expand All @@ -21,23 +28,24 @@ function internalTransfer(
tokenAddress: Address
): void {
// get bank extension bindings
let registry = BankExtension.bind(extensionAddress);
let daoAddress = registry.dao();
let bankRegistry = BankExtension.bind(extensionAddress);

let daoAddress = bankRegistry.dao();

// check if the DAO has an ERC20 extension and assign members balance
if (memberAddress.toHex() != TOTAL.toHex()) {
internalERC20Balance(daoAddress, memberAddress);
}

let tributeDaos: string[] = [];

if (
TOTAL.toHex() != memberAddress.toHex() &&
GUILD.toHex() != memberAddress.toHex()
GUILD.toHex() != memberAddress.toHex() &&
UNITS.toHex() != memberAddress.toHex() &&
MEMBER_COUNT.toHex() != memberAddress.toHex()
) {
let membertokenBalanceId = daoAddress
.toHex()
.concat("-tokenbalances-")
.concat(memberAddress.toHex());

let member = Member.load(memberAddress.toHex());
let token = Token.load(tokenAddress.toHex());
let tokenBalance = TokenBalance.load(membertokenBalanceId);

if (member == null) {
member = new Member(memberAddress.toHex());
Expand All @@ -55,49 +63,12 @@ function internalTransfer(
// add members daos
member.tributeDaos = tributeDaos;

if (token == null) {
token = new Token(tokenAddress.toHex());
token.tokenAddress = tokenAddress;

// get additional ERC20 info
let erc20Registry = ERC20.bind(tokenAddress);

// try get the token name
let callResult_name = erc20Registry.try_name();
if (callResult_name.reverted) {
log.info("try_name reverted", []);
} else {
token.name = callResult_name.value;
}

// try get the token symbol
let callResult_symbol = erc20Registry.try_symbol();
if (callResult_symbol.reverted) {
log.info("try_symbol reverted", []);
} else {
token.symbol = callResult_symbol.value;
}
}

if (tokenBalance == null) {
tokenBalance = new TokenBalance(membertokenBalanceId);
// we give it an initial 0 balance
tokenBalance.tokenBalance = BigInt.fromI32(0);
tokenBalance.tributeDao = daoAddress.toHex();

let bankId = daoAddress
.toHex()
.concat("-bank-")
.concat(extensionAddress.toHex());
tokenBalance.bank = bankId;
}

/**
* get `balanceOf` for members UNITS
*/

// get balanceOf member units
let balanceOfUNITS = registry.balanceOf(memberAddress, UNITS);
let balanceOfUNITS = bankRegistry.balanceOf(memberAddress, UNITS);
member.units = balanceOfUNITS;

// omit the `TOTAL` & `GUILD` addresses from the ragequit check
Expand All @@ -111,18 +82,11 @@ function internalTransfer(
member.didFullyRagequit = didFullyRagequit;
}

tokenBalance.token = tokenAddress.toHex();
tokenBalance.member = memberAddress.toHex();

tokenBalance.tokenBalance = balanceOfUNITS;

member.save();
token.save();
tokenBalance.save();
}

// get totalUnits in the dao
let balanceOfTotalUnits = registry.balanceOf(TOTAL, UNITS);
let balanceOfTotalUnits = bankRegistry.balanceOf(TOTAL, UNITS);
let dao = TributeDao.load(daoAddress.toHexString());

if (dao != null) {
Expand All @@ -132,6 +96,69 @@ function internalTransfer(
}
}

function internalERC20Balance(
daoAddress: Address,
memberAddress: Address
): void {
// check and get ERC20 extension address
let erc20ExtensionId = daoAddress
.toHex()
.concat("-extension-")
.concat(ERC20_EXTENSION_ID);
let erc20Extension = Extension.load(erc20ExtensionId);

if (erc20Extension != null) {
let erc20ExtensionRegistry = ERC20Extension.bind(
Address.fromString(erc20Extension.extensionAddress.toHexString())
);

// erc20 symbol
let symbol = erc20ExtensionRegistry.symbol();
let totalSupply = erc20ExtensionRegistry.totalSupply();
let name = erc20ExtensionRegistry.name();

let balance = erc20ExtensionRegistry.balanceOf(memberAddress);

let tokenId = daoAddress
.toHex()
.concat("-token-")
.concat(erc20Extension.extensionAddress.toHex());

let token = Token.load(tokenId);

if (token == null) {
token = new Token(tokenId);

token.symbol = symbol;
token.name = name;
token.tokenAddress = erc20Extension.extensionAddress;
}

token.totalSupply = totalSupply;

token.save();

// update holder
let tokenHolderId = daoAddress
.toHex()
.concat("-tokenholder-")
.concat(memberAddress.toHex());

let tokenHolder = TokenHolder.load(tokenHolderId);

if (tokenHolder == null) {
tokenHolder = new TokenHolder(tokenHolderId);

tokenHolder.member = memberAddress.toHex();
tokenHolder.token = tokenId;
}

tokenHolder.balance = balance;

tokenHolder.save();
}
}

export function handleNewBalance(event: NewBalance): void {
log.info(
"================ NewBalance event fired. member {}, tokenAddr {}, amount {}",
Expand Down
7 changes: 7 additions & 0 deletions subgraph/mappings/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Address } from "@graphprotocol/graph-ts";

// Reserved Internal Addresses
export let UNITS: Address = Address.fromString(
"0x00000000000000000000000000000000000Ff1CE"
);
Expand All @@ -9,6 +10,9 @@ export let GUILD: Address = Address.fromString(
export let TOTAL: Address = Address.fromString(
"0x000000000000000000000000000000000000babe"
);
export let MEMBER_COUNT: Address = Address.fromString(
"0x00000000000000000000000000000000decafbad"
);

// adapter/extension names hashed (lowercase)
export let BANK_EXTENSION_ID: string =
Expand Down Expand Up @@ -40,3 +44,6 @@ export let VOTING_ID: string =

export let NFT_EXTENSION_ID: string =
"0x7dd481eb4b63b94bb55e6b98aabb06c3b8484f82a4d656d6bca0b0cf9b446be0";

export let ERC20_EXTENSION_ID: string =
"0x77d63af07d7aad7f422b79cf9d7285aec3f3e6f32e6e4391f1ce842d752663fd";
6 changes: 4 additions & 2 deletions subgraph/mappings/helpers/extension-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ export function loadOrCreateExtensionEntity(
extensionId.toHexString(),
]);

bank(daoAddress, extensionAddress);
BankExtensionTemplate.create(extensionAddress);

bank(daoAddress, extensionAddress);
} else if (NFT_EXTENSION_ID.toString() == extensionId.toHexString()) {
log.info("INFO NFT_EXTENSION_ID, extensionId: {}", [
extensionId.toHexString(),
]);

nft(daoAddress, extensionAddress);
NFTExtensionTemplate.create(extensionAddress);

nft(daoAddress, extensionAddress);
}
}

Expand Down
17 changes: 7 additions & 10 deletions subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Member @entity {
createdAt: String
delegateKey: Bytes
units: BigInt!
tokenBalances: [TokenBalance!] @derivedFrom(field: "member")
tokenHoldings: [TokenHolder!] @derivedFrom(field: "member")
proposals: [Proposal!] @derivedFrom(field: "member")
didFullyRagequit: Boolean
isDelegated: Boolean
Expand Down Expand Up @@ -159,7 +159,6 @@ type Bank @entity {
id: ID! # Set to `${tribute.id}-bank-${bank.id}`
"The bank of the DAO"
bankAddress: Bytes!
tokenBalances: [TokenBalance!] @derivedFrom(field: "bank")
tributeDao: TributeDao!
}

Expand All @@ -169,17 +168,15 @@ type Token @entity {
name: String
symbol: String
tokenAddress: Bytes!
balance: BigInt
totalSupply: BigInt
holders: [TokenHolder!] @derivedFrom(field: "token")
}

type TokenBalance @entity {
"Unique identifier and primary key of the `TokenBalance` entity"
id: ID! # Set to `${tribute.id}-tokenbalance-${tokenBalance.id}`
token: Token! #[Token!]
tokenBalance: BigInt!
type TokenHolder @entity {
id: ID! # Set to `${tribute.id}-tokenholder-${memberAddress.id}`
member: Member
bank: Bank
tributeDao: TributeDao
balance: BigInt
token: Token #`${tribute.id}-token-${token.id}`
}

type NFT @entity {
Expand Down
4 changes: 3 additions & 1 deletion subgraph/subgraph-deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,16 @@ ${couponOnboardingYAML({
apiVersion: 0.0.4
language: wasm/assemblyscript
entities:
- TokenBalance
- TokenHolder
- Token
- Member
abis:
- name: BankExtension
file: ../build/contracts/BankExtension.json
- name: ERC20
file: ../build/contracts/ERC20.json
- name: ERC20Extension
file: ../build/contracts/ERC20Extension.json
eventHandlers:
- event: NewBalance(address,address,uint160)
handler: handleNewBalance
Expand Down
4 changes: 3 additions & 1 deletion subgraph/subgraph-tests/helpers/YAML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,16 @@ export const getYAML = ({ daoFactoryAddress }: GetYAMLType): string => {
apiVersion: 0.0.4
language: wasm/assemblyscript
entities:
- TokenBalance
- TokenHolder
- Token
- Member
abis:
- name: BankExtension
file: ../build/contracts/BankExtension.json
- name: ERC20
file: ../build/contracts/ERC20.json
- name: ERC20Extension
file: ../build/contracts/ERC20Extension.json
eventHandlers:
- event: NewBalance(address,address,uint160)
handler: handleNewBalance
Expand Down
Loading

0 comments on commit 64d30c1

Please sign in to comment.