Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
128 changes: 128 additions & 0 deletions apy_trace.log

Large diffs are not rendered by default.

704 changes: 704 additions & 0 deletions apy_trace_human_commented.md

Large diffs are not rendered by default.

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
}
Loading