Перейти к содержанию

Strategy Architecture

Обзор

Technical architecture документация для investment strategy system в Saga DeFi Platform - от конфигурации до blockchain integration.

Architecture Layers

┌─────────────────────────────────────────────────────────────┐
│                  Strategy Architecture                       │
│                                                              │
│  ┌────────────────────────────────────────────────────┐    │
│  │         Configuration Layer (UnifiedConfig)        │    │
│  │         config/limits.yaml → strategies[]          │    │
│  └────────────────────────────────────────────────────┘    │
│                          ↓                                  │
│  ┌────────────────────────────────────────────────────┐    │
│  │         Backend Service Layer                      │    │
│  │         InvestmentService, StrategyValidator       │    │
│  └────────────────────────────────────────────────────┘    │
│                          ↓                                  │
│  ┌────────────────────────────────────────────────────┐    │
│  │         Blockchain Layer                           │    │
│  │         StakingProtocol5/10/20 Contracts           │    │
│  └────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Strategy Configuration Schema

UnifiedConfig Structure

File: config/limits.yaml

limits:
  investment_strategies:
    - id: 1
      name: "Conservative"
      apy: 5.0
      risk_level: "low"
      contract_address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6"
      implementation_address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853"
      st_token_address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
      min_amount: "10.00"
      max_amount: "10000.00"
      enabled: true
      recommended_for:
        - "beginners"
        - "low_risk_tolerance"
      compound_frequency: "daily"  # Interest compounds daily

    - id: 2
      name: "Balanced"
      apy: 10.0
      risk_level: "medium"
      contract_address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788"
      implementation_address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"
      st_token_address: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"
      min_amount: "10.00"
      max_amount: "10000.00"
      enabled: true
      recommended_for:
        - "experienced"
        - "medium_risk_tolerance"
      compound_frequency: "daily"

    - id: 3
      name: "Aggressive"
      apy: 20.0
      risk_level: "high"
      contract_address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0"
      implementation_address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
      st_token_address: "0x0165878A594ca255338adfa4d48449f69242Eb8F"
      min_amount: "10.00"
      max_amount: "10000.00"
      enabled: true
      recommended_for:
        - "expert_traders"
        - "high_risk_tolerance"
      compound_frequency: "daily"

Go Model Structure

// backend/config/unified_config.go
type InvestmentStrategy struct {
    ID                     int      `yaml:"id" json:"id"`
    Name                   string   `yaml:"name" json:"name"`
    APY                    float64  `yaml:"apy" json:"apy"`
    RiskLevel              string   `yaml:"risk_level" json:"risk_level"`
    ContractAddress        string   `yaml:"contract_address" json:"contract_address"`
    ImplementationAddress  string   `yaml:"implementation_address" json:"implementation_address"`
    StTokenAddress         string   `yaml:"st_token_address" json:"st_token_address"`
    MinAmount              string   `yaml:"min_amount" json:"min_amount"`
    MaxAmount              string   `yaml:"max_amount" json:"max_amount"`
    Enabled                bool     `yaml:"enabled" json:"enabled"`
    RecommendedFor         []string `yaml:"recommended_for" json:"recommended_for"`
    CompoundFrequency      string   `yaml:"compound_frequency" json:"compound_frequency"`
}

// UnifiedConfig holds all strategies
type UnifiedConfig struct {
    InvestmentStrategies []InvestmentStrategy `yaml:"investment_strategies"`
}

// Accessor methods
func (cfg *UnifiedConfig) GetInvestmentStrategy(id int) *InvestmentStrategy {
    for _, strategy := range cfg.InvestmentStrategies {
        if strategy.ID == id {
            return &strategy
        }
    }
    return nil
}

func (cfg *UnifiedConfig) GetAllStrategies() []InvestmentStrategy {
    strategies := []InvestmentStrategy{}
    for _, strategy := range cfg.InvestmentStrategies {
        if strategy.Enabled {
            strategies = append(strategies, strategy)
        }
    }
    return strategies
}

Backend Services Architecture

Strategy Validator Service

// backend/shared/services/strategy_validator.go
type StrategyValidator struct {
    config *config.UnifiedConfig
    logger *logging.CanonicalLogger
}

func NewStrategyValidator(cfg *config.UnifiedConfig) *StrategyValidator {
    return &StrategyValidator{
        config: cfg,
        logger: logging.GetGlobalCanonicalLogger(),
    }
}

// Validate strategy selection
func (sv *StrategyValidator) ValidateStrategy(strategyID int, amount SafeDecimal) error {
    // 1. Load strategy from config
    strategy := sv.config.GetInvestmentStrategy(strategyID)
    if strategy == nil {
        return errors.New("invalid strategy ID")
    }

    // 2. Check if strategy is enabled
    if !strategy.Enabled {
        return errors.New("strategy is currently disabled")
    }

    // 3. Validate amount limits
    minAmount := SafeDecimalFromString(strategy.MinAmount)
    maxAmount := SafeDecimalFromString(strategy.MaxAmount)

    if amount.LessThan(minAmount) {
        return fmt.Errorf("amount below minimum: $%s (min: $%s)",
            amount.String(),
            minAmount.String(),
        )
    }

    if amount.GreaterThan(maxAmount) {
        return fmt.Errorf("amount exceeds maximum: $%s (max: $%s)",
            amount.String(),
            maxAmount.String(),
        )
    }

    // 4. Validate blockchain contract
    if !sv.isValidContractAddress(strategy.ContractAddress) {
        return errors.New("invalid strategy contract address")
    }

    return nil
}

func (sv *StrategyValidator) isValidContractAddress(address string) bool {
    // Check Ethereum address format
    return common.IsHexAddress(address) && len(address) == 42
}

// Get strategy recommendation based on user profile
func (sv *StrategyValidator) RecommendStrategy(userProfile UserProfile) (*InvestmentStrategy, error) {
    strategies := sv.config.GetAllStrategies()

    for _, strategy := range strategies {
        if sv.matchesProfile(strategy, userProfile) {
            return &strategy, nil
        }
    }

    // Default: Conservative strategy
    return sv.config.GetInvestmentStrategy(1), nil
}

func (sv *StrategyValidator) matchesProfile(strategy InvestmentStrategy, profile UserProfile) bool {
    // Match based on recommended_for tags
    for _, recommendation := range strategy.RecommendedFor {
        if contains(profile.Tags, recommendation) {
            return true
        }
    }
    return false
}

Investment Service Integration

// backend/shared/services/investment_service.go
type InvestmentService struct {
    config    *config.UnifiedConfig
    validator *StrategyValidator
    repo      *InvestmentRepository
    blockchain *BlockchainStakingService
}

func (s *InvestmentService) CreateInvestment(ctx context.Context, userID int64, req InvestmentRequest) (*Investment, error) {
    // 1. Validate strategy and amount
    err := s.validator.ValidateStrategy(req.StrategyID, req.Amount)
    if err != nil {
        return nil, err
    }

    // 2. Load strategy configuration
    strategy := s.config.GetInvestmentStrategy(req.StrategyID)

    // 3. Create investment record
    investment := &Investment{
        UserID:     userID,
        StrategyID: strategy.ID,
        Amount:     req.Amount,
        APY:        strategy.APY,
        Status:     "pending",
        CreatedAt:  time.Now().UTC(),
    }

    // 4. Save to database
    err = s.repo.CreateInvestment(ctx, investment)
    if err != nil {
        return nil, err
    }

    // 5. Initiate blockchain staking
    go s.blockchain.StakeTokens(investment, strategy)

    s.logger.InfoStructured("Investment created",
        "investment_id", investment.ID,
        "strategy_id", strategy.ID,
        "strategy_name", strategy.Name,
        "apy", strategy.APY,
        "amount", req.Amount.String(),
    )

    return investment, nil
}

Blockchain Integration Architecture

Smart Contract Mapping

// backend/shared/services/blockchain_staking_service.go
type BlockchainStakingService struct {
    config *config.UnifiedConfig
    logger *logging.CanonicalLogger
}

func (s *BlockchainStakingService) StakeTokens(investment *Investment, strategy *InvestmentStrategy) {
    // 1. Connect to blockchain
    client, err := ethclient.Dial(s.config.GetBlockchainRPCURL())
    if err != nil {
        s.markInvestmentFailed(investment, err)
        return
    }

    // 2. Load staking protocol contract (UUPS Proxy)
    stakingAddress := common.HexToAddress(strategy.ContractAddress)
    staking, err := contracts.NewStakingProtocol(stakingAddress, client)
    if err != nil {
        s.markInvestmentFailed(investment, err)
        return
    }

    // 3. Approve USDC для staking
    auth, err := s.getTransactionAuth(client, investment.UserID)
    usdcAddress := common.HexToAddress(s.config.GetUSDCContractAddress())
    usdc, err := contracts.NewTestUSDC(usdcAddress, client)

    amount := investment.Amount.ToBigInt()
    approveTx, err := usdc.Approve(auth, stakingAddress, amount)
    if err != nil {
        s.markInvestmentFailed(investment, err)
        return
    }

    // Wait for approval confirmation
    approveReceipt, err := bind.WaitMined(context.Background(), client, approveTx)
    if err != nil || approveReceipt.Status != types.ReceiptStatusSuccessful {
        s.markInvestmentFailed(investment, errors.New("approval failed"))
        return
    }

    // 4. Stake USDC → receive stTokens
    stakeTx, err := staking.Stake(auth, amount)
    if err != nil {
        s.markInvestmentFailed(investment, err)
        return
    }

    // Wait for stake confirmation
    stakeReceipt, err := bind.WaitMined(context.Background(), client, stakeTx)
    if err != nil || stakeReceipt.Status != types.ReceiptStatusSuccessful {
        s.markInvestmentFailed(investment, errors.New("staking failed"))
        return
    }

    // 5. Update investment status
    investment.Status = "active"
    investment.StakeTxHash = stakeTx.Hash().Hex()
    investment.StakedAt = time.Now().UTC()
    investment.StTokenAddress = strategy.StTokenAddress

    s.repo.UpdateInvestment(context.Background(), investment)

    s.logger.InfoStructured("Investment staked successfully",
        "investment_id", investment.ID,
        "strategy_id", strategy.ID,
        "stake_tx_hash", stakeTx.Hash().Hex(),
        "st_token_address", strategy.StTokenAddress,
    )
}

Interest Calculation Architecture

Smart Contract Side (Continuous Compounding):

// blockchain/contracts/StakingProtocol.sol
contract StakingProtocol is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public apy; // e.g., 5 for 5%, 10 for 10%, 20 for 20%

    // Simulate one day of interest accrual
    function simulateDay() external {
        uint256 totalStaked = usdcToken.balanceOf(address(this));

        // Daily interest = (totalStaked * apy) / 365 / 100
        uint256 interest = (totalStaked * apy) / 365 / 100;

        // Mint new USDC to cover interest
        usdcToken.mint(address(this), interest);

        // Exchange rate updates automatically through stToken rebase
        // Users' stToken balances remain same, but value increases
    }
}

Backend Calculation (Query Current Value):

// backend/shared/services/investment_value_service.go
func (s *InvestmentValueService) GetCurrentValue(ctx context.Context, investment *Investment) (SafeDecimal, error) {
    // Load strategy to get stToken address
    strategy := s.config.GetInvestmentStrategy(investment.StrategyID)

    // Connect to blockchain
    client, err := ethclient.Dial(s.config.GetBlockchainRPCURL())

    // Load stToken contract
    stTokenAddress := common.HexToAddress(strategy.StTokenAddress)
    stToken, err := contracts.NewStToken(stTokenAddress, client)

    // Get user's stToken balance
    userAddress := s.getUserWalletAddress(ctx, investment.UserID)
    stBalance, err := stToken.BalanceOf(&bind.CallOpts{}, common.HexToAddress(userAddress))

    // Get exchange rate (stToken → USDC)
    exchangeRate, err := stToken.ExchangeRate(&bind.CallOpts{})

    // Calculate current value
    // currentValue = stBalance * exchangeRate / 1e18
    currentValue := new(big.Int).Mul(stBalance, exchangeRate)
    currentValue.Div(currentValue, big.NewInt(1e18))

    return SafeDecimalFromBigInt(currentValue, 6), nil
}

API Layer Architecture

Strategy Endpoints

// backend/shared/routing/strategy_handler.go
type StrategyHandler struct {
    config *config.UnifiedConfig
}

// GET /api/strategies
func (h *StrategyHandler) GetAllStrategies(w http.ResponseWriter, r *http.Request) {
    strategies := h.config.GetAllStrategies()

    // Transform для frontend (hide internal fields)
    publicStrategies := make([]StrategyResponse, len(strategies))
    for i, strategy := range strategies {
        publicStrategies[i] = StrategyResponse{
            ID:        strategy.ID,
            Name:      strategy.Name,
            APY:       strategy.APY,
            RiskLevel: strategy.RiskLevel,
            MinAmount: strategy.MinAmount,
            MaxAmount: strategy.MaxAmount,
        }
    }

    http_helpers.SendSuccess(w, publicStrategies)
}

// GET /api/strategies/{id}
func (h *StrategyHandler) GetStrategy(w http.ResponseWriter, r *http.Request) {
    strategyID, _ := strconv.Atoi(chi.URLParam(r, "id"))

    strategy := h.config.GetInvestmentStrategy(strategyID)
    if strategy == nil {
        http_helpers.SendError(w, "Strategy not found", http.StatusNotFound)
        return
    }

    response := StrategyResponse{
        ID:        strategy.ID,
        Name:      strategy.Name,
        APY:       strategy.APY,
        RiskLevel: strategy.RiskLevel,
        MinAmount: strategy.MinAmount,
        MaxAmount: strategy.MaxAmount,
        CompoundFrequency: strategy.CompoundFrequency,
    }

    http_helpers.SendSuccess(w, response)
}

type StrategyResponse struct {
    ID                int     `json:"id"`
    Name              string  `json:"name"`
    APY               float64 `json:"apy"`
    RiskLevel         string  `json:"risk_level"`
    MinAmount         string  `json:"min_amount"`
    MaxAmount         string  `json:"max_amount"`
    CompoundFrequency string  `json:"compound_frequency"`
}

Testing Architecture

Unit Tests

// backend/tests/unit/strategy_validator_test.go
func TestStrategyValidator_ValidateStrategy(t *testing.T) {
    cfg := loadTestConfig()
    validator := NewStrategyValidator(cfg)

    tests := []struct {
        name       string
        strategyID int
        amount     SafeDecimal
        wantError  bool
    }{
        {
            name:       "Valid Conservative strategy",
            strategyID: 1,
            amount:     SafeDecimalFromString("100.00"),
            wantError:  false,
        },
        {
            name:       "Amount below minimum",
            strategyID: 1,
            amount:     SafeDecimalFromString("5.00"),
            wantError:  true,
        },
        {
            name:       "Amount above maximum",
            strategyID: 1,
            amount:     SafeDecimalFromString("50000.00"),
            wantError:  true,
        },
        {
            name:       "Invalid strategy ID",
            strategyID: 999,
            amount:     SafeDecimalFromString("100.00"),
            wantError:  true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := validator.ValidateStrategy(tt.strategyID, tt.amount)
            if (err != nil) != tt.wantError {
                t.Errorf("ValidateStrategy() error = %v, wantError %v", err, tt.wantError)
            }
        })
    }
}

Integration Tests

// backend/tests/integration/strategy_lifecycle_test.go
func TestStrategyLifecycle(t *testing.T) {
    server := setupTestServer()
    defer server.Close()

    // 1. Get all strategies
    resp := makeRequest(server, "GET", "/api/strategies", nil)
    var strategies []StrategyResponse
    json.Unmarshal(resp.Body, &strategies)

    assert.Equal(t, 3, len(strategies))
    assert.Equal(t, "Conservative", strategies[0].Name)
    assert.Equal(t, 5.0, strategies[0].APY)

    // 2. Create investment with strategy
    investmentReq := InvestmentRequest{
        StrategyID: 2, // Balanced
        Amount:     SafeDecimalFromString("500.00"),
    }

    investResp := makeAuthRequest(server, "POST", "/api/investments", investmentReq, userToken)
    var investment Investment
    json.Unmarshal(investResp.Body, &investment)

    assert.Equal(t, int64(2), investment.StrategyID)
    assert.Equal(t, 10.0, investment.APY)

    // 3. Wait for blockchain staking
    time.Sleep(15 * time.Second)

    // 4. Verify investment active
    getResp := makeAuthRequest(server, "GET",
        fmt.Sprintf("/api/investments/%d", investment.ID),
        nil, userToken)

    var activeInvestment Investment
    json.Unmarshal(getResp.Body, &activeInvestment)
    assert.Equal(t, "active", activeInvestment.Status)
}



📋 Метаданные

Версия: 2.4.82

Обновлено: 2025-10-21

Статус: Published