mirror of
https://github.com/FlipsideCrypto/mkr-analysis.git
synced 2026-02-06 10:56:56 +00:00
476 lines
17 KiB
Plaintext
476 lines
17 KiB
Plaintext
---
|
|
title: "Understanding MKR spread for Delegate Targeting"
|
|
author: "Carlos Mercado"
|
|
date: '2022-08-30'
|
|
output:
|
|
html_document:
|
|
code_folding: hide
|
|
editor_options:
|
|
chunk_output_type: console
|
|
markdown:
|
|
wrap: 72
|
|
---
|
|
|
|
# Intro
|
|
|
|
Flipside's governance team works deeply with the MakerDAO team to
|
|
create, discuss, and vote on proposals that ultimately improve Maker's
|
|
market position and revenue model. In order to increase our influence at
|
|
Maker, we seek delegation of MKR to our voting address.
|
|
|
|
This markdown details use of the new Flipside `ethscore` package to
|
|
identify potential addresses to target to request/earn delegation of
|
|
their MKR to our voting address.
|
|
|
|
# Package Requirements
|
|
|
|
ethscore uses shroomDK to access Flipside data for its analysis. The
|
|
best way to install these packages is via devtools install_github().
|
|
|
|
```{r, eval = FALSE, message = FALSE, warning= FALSE}
|
|
# This chunk does not eval
|
|
# library(devtools) # install if you haven't already
|
|
# devtools::install_github(repo = 'FlipsideCrypto/sdk', subdir = 'r/shroomDK')
|
|
# devtools::install_github(repo = 'FlipsideCrypto/ethscore')
|
|
```
|
|
|
|
# Addressable Market of MKR Delegation
|
|
|
|
Not all holders of an ERC20 are externally owned accounts (EOAs). Some
|
|
are contract addresses, others are gnosis-safes. Among EOAs, there are
|
|
central exchange managed EOAs for coordinating deposits and withdrawals
|
|
on and off chain which would be inappropriate targets for delegation.
|
|
Also some EOAs are 'cold storage' in that they hold a balance but have
|
|
never initiated a transaction. If an EOA has never done a transaction,
|
|
it is unlikely its first will be delegation of a token which requires
|
|
approvals and other contract interactions that the user may find risky.
|
|
|
|
Thus, for the purposes of growing our delegation, it is imperative we
|
|
understand the addressable market as:
|
|
|
|
- EOAs that are active and likely human owned
|
|
- Gnosis safes, e.g., DAO multi-sigs.
|
|
- MKR in the delegate contract(s)
|
|
- Note: this is 'pvp' in that each MKR we are delegated from this contract is explicitly a MKR taken from another delegate. While we support competition, our first goal is to activate more MKR, not simply fight over a fixed pool.
|
|
|
|
## Current balance of MKR held by those with 1+ MKR
|
|
|
|
'Dust' is common in crypto. Users make swaps of non-integer sizes and
|
|
end up with balances that use many of the 18 decimals permitted by
|
|
ERC20s, e.g., having 0.0042069 MKR (\~ \$3 at time of writing). This
|
|
naturally inflates the 'holders' number we commonly see in tools like
|
|
etherscan.
|
|
|
|

|
|
|
|
```{r, message = FALSE, warning= FALSE}
|
|
library(shroomDK)
|
|
library(ethscore)
|
|
library(scales)
|
|
library(gmp)
|
|
library(dplyr)
|
|
library(reactable)
|
|
library(plotly)
|
|
|
|
mkr <- tolower("0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2")
|
|
total_supply <- 977631
|
|
total_holders <- 87789
|
|
|
|
max_block <- 15440000 # August 30th 11AM UTC
|
|
api_key <- readLines("api_key.txt") # get a free key @ https://sdk.flipsidecrypto.xyz/shroomdk
|
|
|
|
mkr_balances <- address_token_balance(token_address = mkr, min_tokens = 1,
|
|
block_max = max_block, api_key = api_key)
|
|
|
|
plot_ly(mkr_balances, x = ~ADDRESS_TYPE, y = ~log(NEW_VALUE), color = ~ADDRESS_TYPE,
|
|
boxpoints = "all", jitter = 0.3,
|
|
hoverinfo = 'text',
|
|
hovertext = ~paste0(
|
|
'Log-MKR Balance: ',
|
|
round(log(NEW_VALUE), 2),'\n Raw MKR Balance: ',
|
|
scales::label_comma()(floor(NEW_VALUE))
|
|
),
|
|
type = 'box') %>%
|
|
layout(title = '\nDistribution of MKR among those with 1+ MKR',
|
|
xaxis = list(title = 'Address Type'),
|
|
yaxis = list(title = 'LOG(MKR Balance)')
|
|
)
|
|
|
|
num_active_eoas <- nrow(mkr_balances %>% dplyr::filter(ADDRESS_TYPE == "EOA"))
|
|
mkr_held_by_active_eoas <- mkr_balances %>%
|
|
dplyr::filter(ADDRESS_TYPE == "EOA") %>%
|
|
summarise(sum(NEW_VALUE)) %>%
|
|
as.numeric() %>% floor()
|
|
|
|
```
|
|
|
|
Of the `r scales::label_comma()(total_holders)` Holders of MKR, only
|
|
`r label_comma()(nrow(mkr_balances))` (
|
|
`r floor(100*nrow(mkr_balances)/total_holders)`%) have at least 1 whole
|
|
MKR (\~\$700 at time of writing).
|
|
|
|
Given that MKR uses on-chain voting on the Ethereum Layer 1, it may be
|
|
cost prohibitive in ETH gas terms for smaller holders to delegate and
|
|
vote on-chain (as opposed to an off-chain tool like Snapshot).
|
|
|
|
Because holdings of most ERC20s is highly skewed (i.e., most addresses
|
|
have very few MKR and a few have very large amounts) a LOG scale is used
|
|
to more cleanly see differences in the distribution of MKR across
|
|
Address Types.
|
|
|
|
The key insights to note:
|
|
|
|
- Non-targets like contracts, cold storage EOAs, and central exchange EOAs have a wide variance in their MKR holdings.
|
|
- The top holders of MKR are contracts and central exchange EOAs.
|
|
|
|
In practical terms, of the `r label_comma()(total_supply)` MKR total
|
|
supply held by `r label_comma()(total_holders)` holders (Etherscan
|
|
above), there are only `r label_comma()(num_active_eoas)` EOAs active
|
|
and with enough MKR (1+) to be delegate targets. These EOAs hold
|
|
`r label_comma()(mkr_held_by_active_eoas)` MKR, only
|
|
`r floor(100*mkr_held_by_active_eoas/total_supply)`% of supply.
|
|
|
|
```{r}
|
|
mkr_smmry <- mkr_balances %>% group_by(ADDRESS_TYPE) %>%
|
|
summarise(num = n(),
|
|
total = scales::label_comma()(floor(sum(NEW_VALUE))),
|
|
avg = scales::label_comma()(floor(mean(NEW_VALUE))),
|
|
median = scales::label_comma()(floor(median(NEW_VALUE))),
|
|
max = scales::label_comma()(floor(max(NEW_VALUE))),
|
|
sd = scales::label_comma()(floor(sd(NEW_VALUE))))
|
|
|
|
reactable(mkr_smmry,
|
|
columns = list(
|
|
ADDRESS_TYPE = colDef(name = 'Address Type'),
|
|
num = colDef(name = 'Count', align = 'right'),
|
|
total = colDef(name = 'Total', align = 'right'),
|
|
avg = colDef(name = 'Average', align = 'right'),
|
|
median = colDef(name = 'Median', align = 'right'),
|
|
max = colDef(name = 'Max', align = 'right'),
|
|
sd = colDef(name = 'Standard Deviation', align = 'right')
|
|
))
|
|
|
|
```
|
|
|
|
```{r}
|
|
|
|
mkr_gov <- floor(mkr_balances[
|
|
mkr_balances$ADDRESS == tolower('0x0a3f6849f78076aefaDf113F5BED87720274dDC0'), "NEW_VALUE"]
|
|
)
|
|
mkr_available <- mkr_gov + mkr_held_by_active_eoas
|
|
```
|
|
|
|
The MKR governance contract:
|
|
`0x0a3f6849f78076aefadf113f5bed87720274ddc0` held
|
|
`r label_comma()(mkr_gov)` MKR as of block `r label_comma()(max_block)`.
|
|
The Largest contract and overall address holder of MKR.
|
|
|
|
This means of the total `r label_comma()(total_supply)` MKR:
|
|
`r label_comma()(mkr_available)`(\~
|
|
`r floor(100*mkr_available/total_supply)`%) is practically available for
|
|
delegation.
|
|
|
|
- `r label_comma()(mkr_held_by_active_eoas)` in EOAs w/ 1+ MKR that are not in the governance contract.
|
|
- `r label_comma()(mkr_gov)` in the governance contract whose delegation can be fought over pvp style.
|
|
- Note: excluding the low number of gnosis safes for now.
|
|
|
|
At time of writing, Flipside Crypto has \~9,000 MKR delegated to it
|
|
(4.7% of current governance).
|
|
|
|
# Time-Weighted MKR Holders
|
|
|
|
Instead of analyzing holders based on current balance on MKR, we can add
|
|
weight for *having held* MKR for a long time. For example, weighing an
|
|
address whose held 10 MKR for 100,000 blocks as a better delegate target
|
|
than one who has held 100 MKR for only 1,000 blocks.
|
|
|
|
Giving users 1 point per MKR for every 1,000 blocks where they held at
|
|
least 0.1 MKR in the range of Jan 1, 2021 (block #:11,566,000) to Aug
|
|
30th 11am UTC (block \# 15,440,000) it is clear there is a strong
|
|
correlation between holding MKR now and having held it in the past- but
|
|
important outliers and nuance allow for more precision in targeting
|
|
potential delegates.
|
|
|
|
```{r, message = FALSE, warning= FALSE}
|
|
|
|
min_block <- 11566000
|
|
|
|
mkr_timeweighted <- address_time_weighted_token_balance(mkr,
|
|
min_tokens = 0.1,
|
|
block_min = min_block,
|
|
block_max = max_block,
|
|
amount_weighting = TRUE,
|
|
api_key = api_key
|
|
)
|
|
|
|
plot_ly(mkr_timeweighted, x = ~ADDRESS_TYPE, y = ~log(TIME_WEIGHTED_SCORE),
|
|
color = ~ADDRESS_TYPE,
|
|
boxpoints = "all", jitter = 0.3,
|
|
hoverinfo = 'text',
|
|
hovertext = ~paste0(
|
|
'Log-MKR TW Score: ',
|
|
round(log(TIME_WEIGHTED_SCORE), 2),
|
|
'\n Raw MKR TW Score: ',
|
|
scales::label_comma()(floor(TIME_WEIGHTED_SCORE))
|
|
),
|
|
type = 'box') %>%
|
|
layout(title = '\nDistribution of MKR Time Weighted Scoring',
|
|
xaxis = list(title = 'Address Type'),
|
|
yaxis = list(title = 'LOG(MKR TW Score)')
|
|
)
|
|
|
|
# merging and imputing current balance 0 to compare tw score and balance
|
|
mkr_tw_bal <- merge(x = mkr_timeweighted[, c("ADDRESS","TIME_WEIGHTED_SCORE","ADDRESS_TYPE")],
|
|
y = mkr_balances[,c("ADDRESS","NEW_VALUE")],
|
|
all.x = TRUE, by = "ADDRESS")
|
|
|
|
mkr_tw_bal$NEW_VALUE[is.na(mkr_tw_bal$NEW_VALUE)] <- 0
|
|
|
|
plot_ly(mkr_tw_bal, x = ~log(NEW_VALUE), y = ~log(TIME_WEIGHTED_SCORE),
|
|
color = ~ADDRESS_TYPE, type = 'scatter',
|
|
hoverinfo = 'text',
|
|
hovertext = ~paste0(
|
|
'RAW-MKR Balance: ',
|
|
scales::label_comma()(floor(NEW_VALUE)),
|
|
'\n Raw MKR TW Score: ',
|
|
scales::label_comma()(floor(TIME_WEIGHTED_SCORE))
|
|
)
|
|
) %>%
|
|
layout(title = '\n Current MKR Balance vs Time-Weighted Score',
|
|
xaxis = list(title = 'LOG(Current MKR Balance)'),
|
|
yaxis = list(title = 'LOG(MKR TW Score)')
|
|
)
|
|
|
|
```
|
|
|
|
# Address Selection
|
|
|
|
```{r, warning=FALSE, message=FALSE}
|
|
|
|
select_eoas <- mkr_tw_bal %>% dplyr::filter(ADDRESS_TYPE == "EOA",
|
|
TIME_WEIGHTED_SCORE >= 10000,
|
|
NEW_VALUE >= 50)
|
|
|
|
plot_ly(select_eoas, x = ~NEW_VALUE, y = ~TIME_WEIGHTED_SCORE,
|
|
color = ~ADDRESS_TYPE, type = 'scatter',
|
|
hoverinfo = 'text',
|
|
hovertext = ~paste0(
|
|
'RAW-MKR Balance: ',
|
|
scales::label_comma()(floor(NEW_VALUE)),
|
|
'\n Raw MKR TW Score: ',
|
|
scales::label_comma()(floor(TIME_WEIGHTED_SCORE))
|
|
)
|
|
) %>%
|
|
layout(title = '\n Current MKR Balance vs Time-Weighted Score',
|
|
xaxis = list(title = 'Current MKR Balance'),
|
|
yaxis = list(title = 'MKR TW Score')
|
|
)
|
|
|
|
```
|
|
|
|
Subsetting to target EOAs (i.e., not central exchange associated nor
|
|
cold storage) with at least 50 MKR and a time-weighted score of 10,000
|
|
(equivalent of holding 100 MKR for 100,000 blocks) results in
|
|
`r nrow(select_eoas)` MKR holders. Unlike previous visuals, the above
|
|
visual is *not* LOG adjusted, so the skew in both amount and time held
|
|
is very noticeable.
|
|
|
|
To assess fitness for direct outreach, the number of transactions, days
|
|
active, and last transaction date are pulled for the select EOAs.
|
|
|
|
```{r, warning = FALSE, message = FALSE}
|
|
query <- {
|
|
"
|
|
with select_tx AS (
|
|
SELECT BLOCK_TIMESTAMP, TX_HASH, FROM_ADDRESS as ADDRESS FROM ethereum.core.fact_transactions
|
|
WHERE FROM_ADDRESS IN ('ADDRESSLIST') AND
|
|
BLOCK_NUMBER >= _MIN_BLOCK_ AND
|
|
BLOCK_NUMBER <= _MAX_BLOCK_
|
|
)
|
|
|
|
SELECT ADDRESS, COUNT(*) as num_tx,
|
|
count(DISTINCT(date_trunc('DAY', block_timestamp))) as num_days,
|
|
MAX(block_timestamp) as last_tx_date FROM
|
|
select_tx
|
|
GROUP BY ADDRESS
|
|
"
|
|
}
|
|
|
|
alist <- paste0(select_eoas$ADDRESS, collapse = "','")
|
|
query <- gsub('ADDRESSLIST', replacement = alist, x = query)
|
|
query <- gsub('_MIN_BLOCK_', replacement = min_block, x = query)
|
|
query <- gsub('_MAX_BLOCK_', replacement = max_block, x = query)
|
|
|
|
select_stats <- auto_paginate_query(query, api_key)
|
|
|
|
eoa_stats <- merge(select_eoas, select_stats, by = 'ADDRESS')
|
|
eoa_stats$LAST_TX_DATE <- as.Date(eoa_stats$LAST_TX_DATE)
|
|
|
|
final_eoa <- eoa_stats %>% dplyr::filter(
|
|
NUM_TX < 100000,
|
|
NUM_DAYS <= 600,
|
|
NUM_DAYS >= 5,
|
|
LAST_TX_DATE > (max(eoa_stats$LAST_TX_DATE) - 180)
|
|
)
|
|
|
|
```
|
|
|
|
Some noticeable issues are present among this selection:
|
|
|
|
- `r sum(eoa_stats$NUM_TX >= 100000)` EOAs with 100,000+ Transactions
|
|
(bots?).
|
|
- `r sum(eoa_stats$NUM_DAYS >= 600)` EOAs active nearly every single
|
|
day in time period (bots?).
|
|
- `r sum(eoa_stats$NUM_DAYS <= 5)` EOAs active less than 5 days in
|
|
time period.
|
|
- `r sum(eoa_stats$LAST_TX_DATE < (max(eoa_stats$LAST_TX_DATE) - 180))`
|
|
EOAs inactive for last 6+ months.
|
|
|
|
Subsetting to addresses with \< 100,000 tx; \< 600 days active; at least
|
|
5 days active; and active within the last 6 months results in
|
|
`r nrow(final_eoa)` addresses holding a total of
|
|
`r label_comma()(sum(final_eoa$NEW_VALUE))` MKR.
|
|
|
|
# Address Contact
|
|
|
|
Address to address communication for direct outreach is still immature.
|
|
To identify potential willingness to be contacted and pitched
|
|
delegation, two outreach avenues are identified:
|
|
|
|
- The Ethereum Name Service of each address (if available) is pulled to see if any addresses willingly doxx their
|
|
social media (e.g., by having their ENS in their Twitter name).
|
|
- Governance activity in other DAOs (i.e., Snapshot votes) for potential reach out via DAO partnerships.
|
|
|
|
## ENS
|
|
|
|
Using ENS NFT address `0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85` a one
|
|
to many test of NFT ownership is identified.
|
|
|
|
```{r, warning = FALSE, message = FALSE}
|
|
|
|
# add these to ethscore lol
|
|
hex_to_bigint <- function(x = "0x5ae081a11c9e42983640ad6d4c5b2fa9dd0a0b886e1def6057c0037c33d1bba5"){
|
|
as.character(as.bigz(x))
|
|
}
|
|
|
|
bigint_to_hex <- function(x = "41104824783848331047501863836715107956672917465157448818057950770477717896101"){
|
|
|
|
fill_hex <- function(x){
|
|
if(nchar(x) == 65){
|
|
x <- gsub("0x","0x0",x)
|
|
return(x)
|
|
}
|
|
return(x)
|
|
}
|
|
|
|
hx <- paste0("0x", as.character(as.bigz(x), b = 16))
|
|
hx <- unlist(lapply(hx, fill_hex))
|
|
|
|
return(hx)
|
|
|
|
}
|
|
|
|
ens_nfts_query <- {
|
|
"
|
|
SELECT BLOCK_NUMBER, NFT_TO_ADDRESS as ADDRESS, TOKENID FROM ethereum.core.ez_nft_transfers
|
|
WHERE NFT_ADDRESS = '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85' AND
|
|
NFT_TO_ADDRESS IN ('ADDRESSLIST')
|
|
"
|
|
}
|
|
|
|
addresslist <- paste0(final_eoa$ADDRESS, collapse = "','")
|
|
ens_nfts_query <- gsub("ADDRESSLIST", addresslist, ens_nfts_query)
|
|
ens <- auto_paginate_query(ens_nfts_query, api_key)
|
|
|
|
ens$HEX_TOKENID <- bigint_to_hex(ens$TOKENID)
|
|
|
|
|
|
ens_query <- {
|
|
"
|
|
SELECT DISTINCT
|
|
event_inputs:name :: STRING as ens_name,
|
|
event_inputs:label :: STRING as hex_tokenid
|
|
from ethereum.core.fact_event_logs
|
|
where
|
|
hex_tokenid IN ('HEXLIST')
|
|
"
|
|
}
|
|
|
|
hexlist <- paste0(ens$HEX_TOKENID, collapse = "','")
|
|
ens_query <- gsub("HEXLIST", hexlist, ens_query)
|
|
|
|
|
|
ens_label <- auto_paginate_query(ens_query, api_key)
|
|
ens_label_full <- ens_label[!is.na(ens_label$ENS_NAME), ]
|
|
ens_label_problem <- ens_label[!(ens_label$HEX_TOKENID %in% ens_label_full$HEX_TOKENID), ]
|
|
|
|
eoa_nfts <- merge(ens, ens_label_full, by = 'HEX_TOKENID', all.x = TRUE)
|
|
|
|
eoa_label <- unique(eoa_nfts[, c("ADDRESS", "ENS_NAME")])
|
|
|
|
eoa_with_names <- merge(final_eoa, eoa_label, by = "ADDRESS", all.x = TRUE, all.y = TRUE)
|
|
|
|
unique_eoa_with_name <- eoa_with_names %>% filter(!is.na(ENS_NAME))
|
|
|
|
|
|
|
|
```
|
|
|
|
Of the `r length(unique(eoa_with_names$ADDRESS))` target EOAs there are
|
|
`r length(unique(unique_eoa_with_name$ADDRESS))` EOAs with at least 1
|
|
ENS. These individuals together have
|
|
`r length(unique(unique_eoa_with_name$ENS_NAME))` ENS names total.
|
|
|
|
|
|
```{r, warning = FALSE, message = FALSE}
|
|
|
|
eoa_name_balance <- eoa_with_names
|
|
eoa_name_balance$has_ENS <- ifelse(is.na(eoa_name_balance$ENS_NAME), "No ENS", "Has ENS")
|
|
eoa_name_balance <- unique(eoa_name_balance[, c("ADDRESS","NEW_VALUE","NUM_TX","NUM_DAYS","LAST_TX_DATE","has_ENS")])
|
|
|
|
plot_ly(eoa_name_balance, x = ~has_ENS, y = ~log(NEW_VALUE), color = ~has_ENS,
|
|
boxpoints = "all", jitter = 0.3,
|
|
hoverinfo = 'text',
|
|
hovertext = ~paste0(
|
|
'LOG MKR Balance:',
|
|
round(log(NEW_VALUE), 2),'\n Raw MKR Balance: ',
|
|
scales::label_comma()(floor(NEW_VALUE))
|
|
),
|
|
type = 'box') %>%
|
|
layout(title = '\nDistribution of MKR among target EOAs with and without ENS',
|
|
xaxis = list(title = 'Has ENS'),
|
|
yaxis = list(title = 'LOG(MKR Balance)')
|
|
)
|
|
|
|
|
|
eoa_name_smmry <- eoa_name_balance %>% group_by(has_ENS) %>%
|
|
summarise(num = n(),
|
|
total = scales::label_comma()(floor(sum(NEW_VALUE))),
|
|
avg = scales::label_comma()(floor(mean(NEW_VALUE))),
|
|
median = scales::label_comma()(floor(median(NEW_VALUE))),
|
|
max = scales::label_comma()(floor(max(NEW_VALUE))),
|
|
sd = scales::label_comma()(floor(sd(NEW_VALUE))))
|
|
|
|
reactable(eoa_name_smmry,
|
|
columns = list(
|
|
has_ENS = colDef(name = 'Has ENS'),
|
|
num = colDef(name = 'Count', align = 'right'),
|
|
total = colDef(name = 'Total', align = 'right'),
|
|
avg = colDef(name = 'Average', align = 'right'),
|
|
median = colDef(name = 'Median', align = 'right'),
|
|
max = colDef(name = 'Max', align = 'right'),
|
|
sd = colDef(name = 'Standard Deviation', align = 'right')
|
|
))
|
|
|
|
write.csv(eoa_with_names, "eoa_w_ens_name.csv")
|
|
write.csv(eoa_name_balance, "unique_eoa_has_ens_or_not.csv")
|
|
|
|
```
|
|
|
|
|
|
## Snapshot
|
|
|
|
To improve identification of addresses, snapshot voting history is assessed to identify
|
|
potential DAO partnerships for boosting delegation.
|
|
|
|
TODO |