That Convex, so hot right now.
We’ve been fielding occasional requests from whales to add their Convex rewards to the Curve Leaderboards. “I’d like to move my huge stacks of tokens to Convex to max out my overflow, but I don’t want to drop on the Curve leaderboard.” First Whale Problems?
We get it though. Convex is great, possibly saving DeFi as we know it!
In working to build out the leaderboard request, we had to dive headlong into the architecture of Convex. Without too many resources yet available for this new protocol, we’ve opted to publish what we’ve put together so far. It’s a bit ugly, but hopefully smarter people can use this as a template and build off of it. If you provide better solutions, we’ll update this article.
We know for a fact that more elegant solutions must be possible. The Convex page loads instantly, presumably they’re using a Graph or caching results on their side. It probably would have been easy to just ask their helpful team, but that would contradict our pioneer attitude.
In contrast, the solution presented here is absurdly slow and a bit hacky. Fortunately, a wonderful feature about the internet is that the quickest way to get a “right” answer is simply to publicly embarrass yourself with a “wrong” answer and watch a million people attack.
Posting dev stuff is also a great way to shake out new subscribers who joined this newsletter hoping for TA (technical analysis that is… T&A we may be able to oblige).
So here’s our short guide to using Brownie to scrape per user reward stats for Convex. If you want to know more about Brownie, we can also recommend a great tutorial.
CVX Staking Rewards
This one’s by far the easiest. If you stake CVX, you can simply call the earned function right from their CVX Rewards contract
def scrape_cvx_staked(address):
# Load Contracts
stake_addr = "0xCF50b810E57Ac33B91dCF525C6ddd9881B139332"
staker = Contract(stake_addr)
reward_token = Contract(staker.cvxCrvToken())
# Reward Token Properties
decimals = 10 ** reward_token.decimals()
symbol = reward_token.symbol()
# Scrape Reward Balance
balance = staker.balanceOf(address) / decimals
earned = staker.earned(address) / decimals
print(f"CVX Balance {balance} earned {earned} {symbol}")
All the rest here is just fluff. Enjoy.
$cvxCRV Rewards
This one is easy enough to just grab two thirds of the juicy rewards you get for giving $CVX your $CRV and staking it as $cvxCRV. The cvxCRV rewards contract has an earned function that tells you $CRV balance plus an extraRewards call you can use to retrieve the $3CRV balance.
This is just two thirds of it though. Near as we could tell, we could not find an obvious endpoint to retrieve the $CVX rewards. At the current prices, this is worth the bulk of the value of the rewards.
When you get your $cvxCRV rewards, line 985 here is the line responsible for getting the CVX tokens.
The lines before get you your $CRV and the lines afterwards get the $3CRV. The line in question calls Booster and then Minter, which converts the number of CVX tokens received as it mints them. We pass the same amount as we receive for $CRV, but we get about half the actual number, the results of their cliff formula.
Unfortunately, these contracts don’t seem to have any endpoint that runs this conversion for you. Ergo our code simply replicates the process — and this code doesn’t consider the amtTillMax variable. Still, it gave the correct values for a sample portfolio, so have at it.
def scrape_cvxcrv_staked(address):
# Load Contracts
reward_addr = "0x3Fe65692bfCD0e6CF84cB1E7d24108E434A7587e"
rewards = Contract(reward_addr)
reward_token = Contract(rewards.rewardToken())
staking_token = Contract(rewards.stakingToken())
staking_dec = 10 ** staking_token.decimals()
# Load CRV Properties
crv = Contract(rewards.rewardToken())
crv_dec = 10 ** reward_token.decimals()
crv_symbol = reward_token.symbol()
# $CRV Rewards
staked_balance = rewards.balanceOf(address) / staking_dec
crv_earned = rewards.earned(address) / crv_dec
print(f"cvxCRV Balance {staked_balance} earned " +
f"{crv_earned} {crv_symbol}")
# Load Tripool Properties
extra_rewards = Contract(rewards.extraRewards(0))
tripool = Contract(extra_rewards.rewardToken())
tri_dec = 10 ** tripool.decimals()
tri_symbol = tripool.symbol()
# Tripool Rewards
tri_balance = extra_rewards.balanceOf(address) / tri_dec
tri_earned = extra_rewards.earned(address) / tri_dec
print(f"cvxCRV Balance {tri_balance} " +
f"earned {tri_earned} {tri_symbol}")
# Load CVX Properties
operator = Contract(rewards.operator())
cvx = Contract(operator.minter())
cvx_symbol = cvx.symbol()
cvx_dec = 10 ** cvx.decimals()
# Replicate minting math
base_balance = rewards.earned(address)
cliff = cvx.totalSupply() / cvx.reductionPerCliff()
total_cliffs = cvx.totalCliffs()
if cliff < total_cliffs:
reduction = total_cliffs - cliff
adj_balance = base_balance * reduction / total_cliffs
cvx_balance = adj_balance / cvx_dec
print(f"cvxCRV Balance {staked_balance} " +
f"earned {cvx_balance} {cvx_symbol}")
CVX Pool Rewards
The main Booster contract has the poolInfo property with an index of all relevant contracts for each Curve pool.
We couldn’t find any endpoint that pulls data in the aggregate, so at the moment the script loops through all pools. This makes it painfully slow to use — we have ~200 entries on our leaderboard, times 37 pools means we have a ceiling to how often we can update.
We also need to calculate the number of CVX earned by replicating the minting math as before. In sum it’s a touch inelegant, but after running it once you can theoretically speed it up by building more advanced caching on top of results.
Anyway, here goes:
def scrape_crv_pools(address):
# Load Contract
pool_addr = "0xF403C135812408BFbE8713b5A23a04b3D48AAE31"
pools = Contract(pool_addr)
# Initialize Counters
total_crv = 0
total_cvx = 0
# Loop Through Pools
for i in range(pools.poolLength()):
# Returns [lp, token, gauge, crvRewards]
_addr = pools.poolInfo(i)[3]
_rewards = Contract(_addr)
# Skip if empty
if _rewards.earned(address) == 0:
continue
# Calculate CRV Rewards
_earned_crv = _rewards.earned(address)
_crv = Contract(_rewards.rewardToken())
_crv_symbol = _crv.symbol()
_crv_dec = 10 ** _crv.decimals()
_crv_adj = _earned_crv / _crv_dec
_bal_token = Contract(_rewards.stakingToken())
_bal_dec = 10 ** _bal_token.decimals()
_bal = _rewards.balanceOf(address) / _bal_dec
total_crv += _crv_adj
print(f"Pool {i} balance {_bal} " +
f"earned {_crv_adj} {_crv_symbol}")
# Load CVX Data
operator = Contract(_rewards.operator())
cvx = Contract(operator.minter())
cvx_dec = 10 ** cvx.decimals()
cvx_symbol = cvx.symbol()
# Replicate minting math
cliff = cvx.totalSupply() / cvx.reductionPerCliff()
total_cliffs = cvx.totalCliffs()
if cliff < total_cliffs:
reduction = total_cliffs - cliff
_adj_bal = _earned_crv * reduction / total_cliffs
_cvx_bal = _adj_bal / cvx_dec
total_cvx += _cvx_bal
print(f"Pool {i} balance {_bal} " +
f"earned {_cvx_bal} {cvx_symbol}")
# Final Summary Totals
print(f"Total CRV: {total_crv}")
print(f"Total CVX: {total_cvx}")
If you caught any bugs or have any ideas to improve, we will update here and credit you!
For more info, check our live market data at https://curvemarketcap.com/ or our subscribe to our daily newsletter at https://curve.substack.com/. Nothing in our newsletter can be construed as financial advice, or even coding advice. Author is a $CRV maximalist and has positions in Convex.