Smart 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:
Entrypoint (Strategy Factory)
Purpose: Central control point for all strategies.
Pattern: Factory + Clone Pattern for gas optimization.
Roles: DEFAULT_ADMIN_ROLE, COMPLIANCE_ROLE.
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:
NOT_INVESTED - initial state
PENDING - investment created, awaiting review
ACCEPTED - passed AML/KYC, tokens issued
REJECTED - declined, funds returned
Last updated