file-signatureSmart Contracts

Investment Strategy System

System of smart contracts for managing investment strategies, supporting multiple tokens, automated AML/KYC checks, and a flexible revenue distribution mechanism. The architecture is based on the Factory pattern, using contract cloning to optimize gas costs.

Key Concepts and Terms

  • Entrypoint: The main factory contract for creating and managing strategies.

  • Strategy: Individual strategy contract, represented as an ERC20 token with extended functionality.

  • Vault: External address (wallet or contract) for holding invested funds.

  • AML/KYC: Procedures for investment compliance checks.

  • Normalized Amount: Converting amounts of different tokens to a unified format (18 decimals) for standardized calculations.

  • Pool Size: Total value of strategy's pool used to calculate investors' shares.

  • Compliance Role: The role managing the acceptance/rejection of investments.

System Architecture

The system consists of two main smart contracts:

  1. Entrypoint (Strategy Factory)

    • Purpose: Central control point for all strategies.

    • Pattern: Factory + Clone Pattern for gas optimization.

    • Roles: DEFAULT_ADMIN_ROLE, COMPLIANCE_ROLE.

  2. Strategy (Strategy Contract)

    • Purpose: Individual investment strategy.

    • Pattern: ERC20 + Upgradeable (via cloning).

    • Features: Prohibition of direct token transfers.

Interaction Scheme

  • Admin → Entrypoint.createStrategy() → Strategy Clone

  • Investor → Strategy.invest() → Pending Investment

  • Compliance → Entrypoint.acceptInvestment() → Mints Shares

  • Compliance → Entrypoint.withdrawRevenue() → Burns Shares

Main Functions

Access Management and Role Model

The system uses OpenZeppelin AccessControl for roles:

  • DEFAULT_ADMIN_ROLE: Full rights for strategy creation and pause management.

  • COMPLIANCE_ROLE: Rights to accept/reject investments and withdraw revenue.

grantRole(bytes32 role, address account)

  • Inputs:

    • role: role identifier (bytes32)

    • account: role recipient address (address)

  • Functionality: Assigns role to a user (DEFAULT_ADMIN_ROLE only)

  • Exceptions:

    • AccessControl: account is missing role (if the caller lacks admin rights)

Strategy Management

createStrategy(address vault, uint256 minInvestmentNormalized, uint64 startDate, uint64 endDate, address[] paymentTokens, string[] metadata)

  • Inputs:

    • vault: fund storage address (address)

    • minInvestmentNormalized: minimum investment in normalized format (uint256)

    • startDate: start date in Unix timestamp (uint64)

    • endDate: end date in Unix timestamp (uint64)

    • paymentTokens: array of supported token addresses (address[])

    • metadata: array of 2 elements: [name, symbol] (string[])

  • Functionality:

    • Creates a new strategy by cloning the base implementation.

    • Initializes strategy with specified parameters.

    • Assigns a unique strategy ID.

    • Stores strategy info in mapping.

  • Event:

    • StrategyCreated(strategyId, strategyContract, vault, minInvestment, startDate, endDate)

  • Exceptions:

    • ZeroAddress(): if vault is the zero address

    • InvalidDate(): if dates are incorrect (startDate ≥ endDate or zero)

    • InvalidAmount(): if minimum investment is zero or metadata != 2 elements

    • InvalidPaymentToken(): if token array is empty

pauseStrategy(uint256 strategyId) / unpauseStrategy(uint256 strategyId)

  • Inputs:

    • strategyId: strategy identifier (uint256)

  • Functionality: Pauses/resumes acceptance of new investments in the strategy.

  • Events: StrategyPaused(strategyId), StrategyUnpaused(strategyId)

  • Exceptions:

    • StrategyNotExist(): if the strategy does not exist

Investment Management

invest(uint256 amount, address paymentToken)

  • Inputs:

    • amount: investment amount in tokens (uint256)

    • paymentToken: token address for investment (address)

  • Functionality:

    • Checks that the token is allowed for the strategy.

    • Checks strategy timing (startDate ≤ now ≤ endDate).

    • Normalizes amount, verifies minimum investment.

    • Transfers tokens to the strategy contract.

    • Creates a pending investment record.

    • Adds the investment to the user's list.

  • Event: Invested(investId, investor, timestamp, paymentToken, amount)

  • Exceptions:

    • TokenNotAllowed(paymentToken): if token unsupported

    • StrategyNotActive(): if strategy is not active by time

    • InvalidAmount(): if amount is less than minimum

acceptInvestment(uint256 strategyId, uint256 investId, uint256 poolSize)

  • Inputs:

    • strategyId: strategy identifier (uint256)

    • investId: investment identifier (uint256)

    • poolSize: total strategy pool size for share calculation (uint256)

  • Functionality:

    • Checks investment status (must be PENDING).

    • Transfers funds to vault.

    • Calculates strategy tokens to mint.

    • Issues tokens to investor.

    • Sets status to ACCEPTED and AMLpassed = true.

  • Event:

    • InvestmentAccepted(strategyId, investId, poolSize) (Entrypoint)

    • InvestmentAccepted(investId, sharesMinted, poolSize) (Strategy)

  • Exceptions:

    • StrategyNotExist(): if strategy does not exist

    • StrategyNotActive(): if strategy is paused

    • InvalidAmount(): if poolSize is zero

    • IncorrectStatus(): if investment not in PENDING status

rejectInvestment(uint256 strategyId, uint256 investId)

  • Inputs:

    • strategyId: strategy identifier (uint256)

    • investId: investment identifier (uint256)

  • Functionality:

    • Checks investment status (must be PENDING).

    • Returns funds to investor.

    • Sets status to REJECTED.

  • Events:

    • InvestmentRejected(strategyId, investId) (Entrypoint)

    • InvestmentRejected(investId) (Strategy)

  • Exceptions:

    • StrategyNotExist(): if strategy does not exist

    • IncorrectStatus(): if investment not in PENDING status

Revenue Management

withdrawRevenue(WithdrawParams params) / batchWithdrawRevenue(WithdrawParams[] params)

  • Inputs:

    • params.strategyId: strategy identifier (uint256)

    • params.investor: investor address (address)

    • params.destination: recipient address (address)

    • params.paymentToken: token for withdrawal (address)

    • params.withdrawAmount: withdrawal amount (uint256)

    • params.sharesBurned: number of tokens to burn (uint256)

  • Functionality:

    • Checks investor token sufficiency.

    • Checks contract fund sufficiency.

    • Burns specified number of strategy tokens.

    • Transfers funds to specified address.

    • Records revenue withdrawal info.

  • Events:

    • RevenueWithdrawn(revenueId, strategyId, investor, paymentToken, amount, sharesBurned) (Entrypoint)

    • RevenueWithdrawn(investor, paymentToken, amount, sharesBurned) (Strategy)

  • Exceptions:

    • StrategyNotExist(): if strategy does not exist

    • InvalidAmount(): if sharesBurned/withdrawAmount = 0

    • InsufficientShares(): if investor has insufficient tokens

    • InsufficientBalance(): if contract balance is insufficient

Extended Functionality

Token Normalization System

The system automatically converts amounts of different tokens to a unified format (18 decimals) for standardized calculations of minimum investments and shares.

normalizeAmount(address token, uint256 amount)

  • Inputs:

    • token – token address (address)

    • amount – original amount (uint256)

  • Functionality:

    • Retrieves the number of decimals for the token

    • Converts the amount to 18 decimals

    • Supports tokens with any number of decimals

  • Returns: Normalized amount (uint256)

Investor Share Calculation

The system uses proportional share calculation based on the pool size.

_calculateShares(uint256 investmentAmount, uint256 poolSize)

  • Inputs:

    • investmentAmount – normalized investment amount (uint256)

    • poolSize – total strategy pool size (uint256)

  • Functionality:

    • For the first investment: returns investmentAmount (1:1)

    • For subsequent investments: (investmentAmount×totalSupply)÷poolSize(investmentAmount×totalSupply)÷poolSize

    • Ensures proportional token distribution

    • Returns: Number of tokens to issue (uint256)

Investment State Management

Each investment follows a lifecycle:

  1. NOT_INVESTED - initial state

  2. PENDING - investment created, awaiting review

  3. ACCEPTED - passed AML/KYC, tokens issued

  4. REJECTED - declined, funds returned

Last updated