Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions common/contracts/convexStakingToken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package contracts

import (
"math/big"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)

// ConvexStakingTokenABI is the ABI used to query convexPoolId() on a Convex staking token.
const ConvexStakingTokenABI = "[{\"inputs\":[],\"name\":\"convexPoolId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]"

// ConvexStakingToken is a minimal binding for convexPoolId() on a Convex staking token.
type ConvexStakingToken struct {
contract *bind.BoundContract
}

// NewConvexStakingToken creates a minimal caller for stakingToken.convexPoolId().
func NewConvexStakingToken(address common.Address, backend bind.ContractBackend) (*ConvexStakingToken, error) {
parsed, err := abi.JSON(strings.NewReader(ConvexStakingTokenABI))
if err != nil {
return nil, err
}
contract := bind.NewBoundContract(address, parsed, backend, backend, backend)
return &ConvexStakingToken{contract: contract}, nil
}

// ConvexPoolId calls convexPoolId() on a staking token.
func (c *ConvexStakingToken) ConvexPoolId(opts *bind.CallOpts) (*big.Int, error) {
var out []interface{}
err := c.contract.Call(opts, &out, "convexPoolId")
if err != nil {
return *new(*big.Int), err
}
out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)
return out0, err
}
38 changes: 38 additions & 0 deletions common/contracts/convexUserVault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package contracts

import (
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)

// ConvexUserVaultABI is the ABI used to query stakingToken() on a Convex user vault.
const ConvexUserVaultABI = "[{\"inputs\":[],\"name\":\"stakingToken\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]"

// ConvexUserVault is a minimal binding for stakingToken() on a Convex user vault.
type ConvexUserVault struct {
contract *bind.BoundContract
}

// NewConvexUserVault creates a minimal caller for userVault.stakingToken().
func NewConvexUserVault(address common.Address, backend bind.ContractBackend) (*ConvexUserVault, error) {
parsed, err := abi.JSON(strings.NewReader(ConvexUserVaultABI))
if err != nil {
return nil, err
}
contract := bind.NewBoundContract(address, parsed, backend, backend, backend)
return &ConvexUserVault{contract: contract}, nil
}

// StakingToken calls stakingToken() on a user vault.
func (c *ConvexUserVault) StakingToken(opts *bind.CallOpts) (common.Address, error) {
var out []interface{}
err := c.contract.Call(opts, &out, "stakingToken")
if err != nil {
return *new(common.Address), err
}
out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address)
return out0, err
}
38 changes: 38 additions & 0 deletions common/contracts/fraxBaseStrategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package contracts

import (
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)

// FraxBaseStrategyABI is the ABI used to query userVault() on Frax base strategies.
const FraxBaseStrategyABI = "[{\"inputs\":[],\"name\":\"userVault\",\"outputs\":[{\"internalType\":\"contract IConvexFrax\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]"

// FraxBaseStrategy is a minimal binding for userVault() on Frax base strategies.
type FraxBaseStrategy struct {
contract *bind.BoundContract
}

// NewFraxBaseStrategy creates a minimal caller for fraxBaseStrategy.userVault().
func NewFraxBaseStrategy(address common.Address, backend bind.ContractBackend) (*FraxBaseStrategy, error) {
parsed, err := abi.JSON(strings.NewReader(FraxBaseStrategyABI))
if err != nil {
return nil, err
}
contract := bind.NewBoundContract(address, parsed, backend, backend, backend)
return &FraxBaseStrategy{contract: contract}, nil
}

// UserVault calls userVault() on a Frax base strategy.
func (f *FraxBaseStrategy) UserVault(opts *bind.CallOpts) (common.Address, error) {
var out []interface{}
err := f.contract.Call(opts, &out, "userVault")
if err != nil {
return *new(common.Address), err
}
out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address)
return out0, err
}
178 changes: 178 additions & 0 deletions processes/apr/apy_trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package apr

import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"

"github.com/ethereum/go-ethereum/common"
)

// APY trace logging helper (opt-in via env vars):
//
// - Enable tracing:
// APY_TRACE=1
//
// - Write to a dedicated file (optional):
// APY_TRACE_PATH=/tmp/apy_trace.log
//
// - If this is a relative path, it is relative to the process working directory.
//
// - If not set, output goes to stderr.
//
// - Filter by chain, vault, and/or strategy (comma-separated):
// APY_TRACE_CHAIN=1,10
// APY_TRACE_VAULT=0xabc...,0xdef...
// APY_TRACE_STRATEGY=0x123...,0x456...
//
// Notes:
// - Filters are case-insensitive and address strings should be hex with 0x prefix.
// - If a filter is set, only matching entries are emitted.
type apyTraceConfig struct {
enabled bool
path string
chains map[string]struct{}
vaults map[string]struct{}
strategies map[string]struct{}
}

var (
apyTraceConfigOnce sync.Once
apyTraceCfg apyTraceConfig

apyTraceFileOnce sync.Once
apyTraceFile *os.File
apyTraceFileErr error
apyTraceFileWarn sync.Once

apyTraceWriteMu sync.Mutex
)

func apyTrace(scope string, chainID uint64, vaultAddr common.Address, strategyAddr common.Address, step string, value interface{}) {
cfg := apyTraceLoadConfig()
if !apyTraceMatches(cfg, chainID, vaultAddr, strategyAddr) {
return
}

line := fmt.Sprintf(
"%s APY_TRACE scope=%s chain=%d vault=%s strategy=%s step=%s value=%v",
time.Now().UTC().Format(time.RFC3339Nano),
scope,
chainID,
vaultAddr.Hex(),
strategyAddr.Hex(),
step,
value,
)

if cfg.path != "" {
if file, err := apyTraceOpenFile(cfg.path); err == nil && file != nil {
apyTraceWriteMu.Lock()
fmt.Fprintln(file, line)
apyTraceWriteMu.Unlock()
return
}
}

fmt.Fprintln(os.Stderr, line)
}

func apyTraceLoadConfig() apyTraceConfig {
apyTraceConfigOnce.Do(func() {
enabled := false
if value, ok := os.LookupEnv("APY_TRACE"); ok {
enabled = apyTraceParseBool(value)
} else if _, ok := os.LookupEnv("APY_TRACE_PATH"); ok {
enabled = true
}

apyTraceCfg = apyTraceConfig{
enabled: enabled,
path: strings.TrimSpace(os.Getenv("APY_TRACE_PATH")),
chains: apyTraceParseList("APY_TRACE_CHAIN"),
vaults: apyTraceParseList("APY_TRACE_VAULT"),
strategies: apyTraceParseList("APY_TRACE_STRATEGY"),
}
})

return apyTraceCfg
}

func apyTraceParseBool(value string) bool {
if strings.TrimSpace(value) == "" {
return true
}
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "t", "yes", "y", "on":
return true
default:
return false
}
}

func apyTraceParseList(envKey string) map[string]struct{} {
raw := strings.TrimSpace(os.Getenv(envKey))
if raw == "" {
return nil
}

items := strings.Split(raw, ",")
values := make(map[string]struct{}, len(items))
for _, item := range items {
value := strings.ToLower(strings.TrimSpace(item))
if value == "" {
continue
}
values[value] = struct{}{}
}

return values
}

func apyTraceMatches(cfg apyTraceConfig, chainID uint64, vaultAddr common.Address, strategyAddr common.Address) bool {
if !cfg.enabled {
return false
}

if len(cfg.chains) > 0 {
chainKey := strings.ToLower(strconv.FormatUint(chainID, 10))
if _, ok := cfg.chains[chainKey]; !ok {
return false
}
}

if len(cfg.vaults) > 0 {
vaultKey := strings.ToLower(vaultAddr.Hex())
if _, ok := cfg.vaults[vaultKey]; !ok {
return false
}
}

if len(cfg.strategies) > 0 {
strategyKey := strings.ToLower(strategyAddr.Hex())
if _, ok := cfg.strategies[strategyKey]; !ok {
return false
}
}

return true
}

func apyTraceOpenFile(path string) (*os.File, error) {
apyTraceFileOnce.Do(func() {
if path == "" {
return
}
apyTraceFile, apyTraceFileErr = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if apyTraceFileErr != nil {
apyTraceFileWarn.Do(func() {
fmt.Fprintf(os.Stderr, "%s APY_TRACE error opening %s: %v\n", time.Now().UTC().Format(time.RFC3339Nano), path, apyTraceFileErr)
})
}
})

return apyTraceFile, apyTraceFileErr
}
2 changes: 1 addition & 1 deletion processes/apr/forward.convex.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func calculateConvexForwardAPY(args TCalculateConvexAPYDataStruct) TStrategyAPY
** of CVX printed, based on the CRV rate for the given gauge. Tldr X crv = Y cvx and we do
** something to gt an APR.
**********************************************************************************************/
crvAPR, cvxAPR, crvAPY, cvxAPY := getCVXPoolAPY(chainID, args.strategy.Address, args.baseAssetPrice)
crvAPR, cvxAPR, crvAPY, cvxAPY := getCVXPoolAPY(chainID, args.vault.Address, args.strategy.Address, args.baseAssetPrice)

/**********************************************************************************************
** Just like curve, Convex can have extra rewards which are incentives/bribes on top of the
Expand Down
50 changes: 48 additions & 2 deletions processes/apr/forward.convex.helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ func getCVXForCRV(chainID uint64, crvEarned *bigNumber.Float) *bigNumber.Float {
**************************************************************************************************/
func getCVXPoolAPY(
chainID uint64,
vaultAddress common.Address,
strategyAddress common.Address,
virtualPoolPrice *bigNumber.Float,
) (*bigNumber.Float, *bigNumber.Float, *bigNumber.Float, *bigNumber.Float) {

crvAPR := bigNumber.NewFloat(0)
cvxAPR := bigNumber.NewFloat(0)
crvAPY := bigNumber.NewFloat(0)
Expand All @@ -157,11 +159,55 @@ func getCVXPoolAPY(
if err != nil {
rewardPID, err = convexStrategyContract.ID(nil)
if err != nil {
rewardPID, err = convexStrategyContract.FraxPid(nil)
warnMissingPID := func(pidErr error) {
logs.Warning("Convex PID not found for vault "+vaultAddress.Hex()+" strategy "+strategyAddress.Hex(), pidErr)
}
fraxBaseStrategy, err := contracts.NewFraxBaseStrategy(strategyAddress, client)
if err != nil {
if os.Getenv("ENVIRONMENT") == "dev" {
logs.Error(`Unable to init fraxBaseStrategy for convex strategy `+strategyAddress.Hex(), err)
}
warnMissingPID(err)
return crvAPR, cvxAPR, crvAPY, cvxAPY
}
userVaultAddress, err := fraxBaseStrategy.UserVault(nil)
if err != nil {
if os.Getenv("ENVIRONMENT") == "dev" {
logs.Error(`Unable to get userVault for fraxBaseStrategy `+strategyAddress.Hex(), err)
}
warnMissingPID(err)
return crvAPR, cvxAPR, crvAPY, cvxAPY
}
userVaultContract, err := contracts.NewConvexUserVault(userVaultAddress, client)
if err != nil {
if os.Getenv("ENVIRONMENT") == "dev" {
logs.Error(`Unable to init userVault contract `+userVaultAddress.Hex(), err)
}
warnMissingPID(err)
return crvAPR, cvxAPR, crvAPY, cvxAPY
}
stakingTokenAddress, err := userVaultContract.StakingToken(nil)
if err != nil {
if os.Getenv("ENVIRONMENT") == "dev" {
logs.Error(`Unable to get stakingToken for userVault `+userVaultAddress.Hex(), err)
}
warnMissingPID(err)
return crvAPR, cvxAPR, crvAPY, cvxAPY
}
stakingTokenContract, err := contracts.NewConvexStakingToken(stakingTokenAddress, client)
if err != nil {
if os.Getenv("ENVIRONMENT") == "dev" {
logs.Error(`Unable to init stakingToken contract `+stakingTokenAddress.Hex(), err)
}
warnMissingPID(err)
return crvAPR, cvxAPR, crvAPY, cvxAPY
}
rewardPID, err = stakingTokenContract.ConvexPoolId(nil)
if err != nil {
if os.Getenv("ENVIRONMENT") == "dev" {
logs.Error(`Unable to get reward PID for convex strategy ` + strategyAddress.Hex())
logs.Error(`Unable to get cvxPoolId for stakingToken `+stakingTokenAddress.Hex(), err)
}
warnMissingPID(err)
return crvAPR, cvxAPR, crvAPY, cvxAPY
}
}
Expand Down