InvestmentManager
Inherits:IINVMANAGER, Initializable, PausableUpgradeable, AccessControlUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable
Manages investment rounds and token vesting for the ecosystem
Implements a secure and upgradeable investment management system
Notes:
security-contact: security@nebula-labs.xyz
copyright: Copyright (c) 2025 Nebula Holding Inc. All rights reserved.
oz-upgrades:
State Variables
MAX_INVESTORS_PER_ROUND
Protects against gas limits when processing all investors during finalization
Maximum number of investors allowed per investment round
uint256 private constant MAX_INVESTORS_PER_ROUND = 50;
MIN_ROUND_DURATION
Ensures rounds are long enough for investor participation (5 days)
Minimum duration for an investment round in seconds
uint256 private constant MIN_ROUND_DURATION = 5 days;
MAX_ROUND_DURATION
Prevents overly long rounds that might block capital (90 days)
Maximum duration for an investment round in seconds
uint256 private constant MAX_ROUND_DURATION = 90 days;
UPGRADE_TIMELOCK_DURATION
Provides a security delay before implementation changes can be executed (3 days)
Duration of the timelock period for contract upgrades in seconds
uint256 private constant UPGRADE_TIMELOCK_DURATION = 3 days;
PAUSER_ROLE
Typically granted to guardian addresses for emergency security actions
Role identifier for addresses authorized to pause contract functions
bytes32 private constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
MANAGER_ROLE
Can perform administrative functions like emergency withdrawals
Role identifier for addresses authorized to manage investment operations
bytes32 private constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
UPGRADER_ROLE
Controls the ability to schedule and execute contract upgrades
Role identifier for addresses authorized to upgrade the contract
bytes32 private constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
DAO_ROLE
Has control over creating investment rounds and major decisions
Role identifier for addresses representing the DAO's governance
bytes32 private constant DAO_ROLE = keccak256("DAO_ROLE");
ecosystemToken
Used for token transfers and vesting distributions
Reference to the ecosystem's ERC20 token
IERC20 internal ecosystemToken;
timelock
Destination for emergency withdrawals and holds elevated permissions
Address of the timelock controller
address public timelock;
treasury
Receives ETH from finalized investment rounds
Address of the treasury contract
address public treasury;
supply
Tracks the supply commitment for all created rounds
Total amount of tokens allocated for investment rounds
uint256 public supply;
version
Incremented on each upgrade to track contract versions
Implementation version of this contract
uint32 public version;
rounds
Core data structure that stores round configurations and status
Array of all investment rounds
Round[] public rounds;
investors
Used to track all participants in each round
Maps round IDs to arrays of investor addresses
mapping(uint32 => address[]) private investors;
investorPositions
Tracks how much each investor has contributed to a round
Maps round IDs and investors to their invested ETH amount
mapping(uint32 => mapping(address => uint256)) private investorPositions;
vestingContracts
Used to track deployed vesting contracts for each investor
Maps round IDs and investors to their vesting contract addresses
mapping(uint32 => mapping(address => address)) private vestingContracts;
investorAllocations
Stores maximum ETH and token amounts for each investor in a round
Maps round IDs and investors to their token allocations
mapping(uint32 => mapping(address => Allocation)) private investorAllocations;
totalRoundAllocations
Used to ensure round allocations don't exceed limits
Maps round IDs to the total token allocation for that round
mapping(uint32 => uint256) private totalRoundAllocations;
pendingUpgrade
Implements the timelock upgrade security pattern
Holds information about a pending contract upgrade
UpgradeRequest public pendingUpgrade;
__gap
Prevents storage collision when adding new variables in upgrades
Reserved storage slots for future upgrades
uint256[18] private __gap;
Functions
validRound
Ensures the provided round ID exists
Note: throws: InvalidRound if roundId is out of bounds
modifier validRound(uint32 roundId);
Parameters
roundId
uint32
The ID of the investment round to check
activeRound
Ensures the round is in active status
Note: throws: RoundNotActive if the round is not active
modifier activeRound(uint32 roundId);
Parameters
roundId
uint32
The ID of the investment round to check
correctStatus
Ensures the round has a specific status
Note: throws: InvalidRoundStatus if the round doesn't have the required status
modifier correctStatus(uint32 roundId, RoundStatus requiredStatus);
Parameters
roundId
uint32
The ID of the investment round to check
requiredStatus
RoundStatus
The status that the round should have
nonZeroAddress
Ensures the provided address is not zero
Note: throws: ZeroAddressDetected if the address is zero
modifier nonZeroAddress(address addr);
Parameters
addr
address
The address to check
constructor
Ensures the provided amount is not zero
Notes:
throws: InvalidAmount if the amount is zero
oz-upgrades-unsafe-allow: constructor
constructor();
receive
Automatically invests in the current active round
Fallback function that handles direct ETH transfers
Note: throws: NoActiveRound if no rounds are currently active
receive() external payable;
initialize
Sets up roles, connects to ecosystem contracts, and initializes version
Initializes the contract with essential parameters
Note: throws: ZeroAddressDetected if any parameter is the zero address
function initialize(address token, address timelock_, address treasury_) external initializer;
Parameters
token
address
Address of the ecosystem token
timelock_
address
Address of the timelock controller
treasury_
address
Address of the treasury contract
pause
Pauses the contract's core functionality
When paused, most state-changing functions will revert
Emergency functions like withdrawals remain active while paused
Only callable by addresses with PAUSER_ROLE
Notes:
requires-role: PAUSER_ROLE
events-emits: {Paused} event from PausableUpgradeable
function pause() external onlyRole(PAUSER_ROLE);
unpause
Unpauses the contract, resuming normal operations
Re-enables all functionality that was blocked during pause
Only callable by addresses with PAUSER_ROLE
Notes:
requires-role: PAUSER_ROLE
events-emits: {Unpaused} event from PausableUpgradeable
function unpause() external onlyRole(PAUSER_ROLE);
scheduleUpgrade
Schedules an upgrade to a new implementation contract
The upgrade cannot be executed until the timelock period has passed
Initiates the timelock period before an upgrade can be executed
Notes:
requires-role: UPGRADER_ROLE
security: Implements a timelock delay for added security
events-emits: {UpgradeScheduled} when upgrade is scheduled
throws: ZeroAddressDetected if newImplementation is zero address
throws: If called by address without UPGRADER_ROLE
function scheduleUpgrade(address newImplementation)
external
onlyRole(UPGRADER_ROLE)
nonZeroAddress(newImplementation);
Parameters
newImplementation
address
Address of the new implementation contract
cancelUpgrade
Cancels a previously scheduled upgrade
Only callable by addresses with UPGRADER_ROLE
function cancelUpgrade() external onlyRole(UPGRADER_ROLE);
emergencyWithdrawToken
Only callable by addresses with MANAGER_ROLE
Emergency function to withdraw all tokens to the timelock
Notes:
throws: ZeroAddressDetected if token address is zero
throws: ZeroBalance if contract has no token balance
function emergencyWithdrawToken(address token) external nonReentrant onlyRole(MANAGER_ROLE) nonZeroAddress(token);
Parameters
token
address
The ERC20 token to withdraw
emergencyWithdrawEther
Only callable by addresses with MANAGER_ROLE
Emergency function to withdraw all ETH to the timelock
Note: throws: ZeroBalance if contract has no ETH balance
function emergencyWithdrawEther() external nonReentrant onlyRole(MANAGER_ROLE);
createRound
Creates a new investment round with specified parameters
Only callable by DAO_ROLE when contract is not paused
Notes:
requires-role: DAO_ROLE
security: Validates all parameters and checks token supply
events-emits: {CreateRound} when round is created
events-emits: {RoundStatusUpdated} when round status is set to PENDING
throws: InvalidDuration if duration is outside allowed range
throws: InvalidEthTarget if ethTarget is zero
throws: InvalidTokenAllocation if tokenAlloc is zero
throws: InvalidStartTime if start is in the past
throws: InsufficientSupply if contract lacks required tokens
function createRound(
uint64 start,
uint64 duration,
uint256 ethTarget,
uint256 tokenAlloc,
uint64 vestingCliff,
uint64 vestingDuration
) external onlyRole(DAO_ROLE) whenNotPaused returns (uint32);
Parameters
start
uint64
Timestamp when the round should start
duration
uint64
Length of the round in seconds
ethTarget
uint256
Target amount of ETH to raise in the round
tokenAlloc
uint256
Total number of tokens allocated for this round
vestingCliff
uint64
Duration in seconds before vesting begins
vestingDuration
uint64
Total duration of the vesting period in seconds
Returns
<none>
uint32
roundId The ID of the newly created round
activateRound
Activates a pending investment round
Only callable by MANAGER_ROLE when contract is not paused
Notes:
requires-role: MANAGER_ROLE
security: Ensures round exists and is in PENDING status
security: Validates round timing constraints
events-emits: {RoundStatusUpdated} when round status is set to ACTIVE
modifiers: validRound, correctStatus(PENDING), whenNotPaused
throws: RoundStartTimeNotReached if current time is before start time
throws: RoundEndTimeReached if current time is after end time
throws: InvalidRound if roundId doesn't exist
throws: InvalidRoundStatus if round is not in PENDING status
function activateRound(uint32 roundId)
external
onlyRole(MANAGER_ROLE)
validRound(roundId)
correctStatus(roundId, RoundStatus.PENDING)
whenNotPaused;
Parameters
roundId
uint32
The ID of the round to activate
addInvestorAllocation
Adds a new investor allocation to a specific investment round
Only callable by MANAGER_ROLE when contract is not paused
Notes:
requires-role: MANAGER_ROLE
security: Validates round status and allocation limits
events-emits: {InvestorAllocated} when allocation is added
modifiers: validRound, whenNotPaused
throws: InvalidInvestor if investor address is zero
throws: InvalidEthAmount if ethAmount is zero
throws: InvalidTokenAmount if tokenAmount is zero
throws: InvalidRoundStatus if round is completed or finalized
throws: AllocationExists if investor already has an allocation
throws: ExceedsRoundAllocation if allocation exceeds round limit
function addInvestorAllocation(uint32 roundId, address investor, uint256 ethAmount, uint256 tokenAmount)
external
onlyRole(MANAGER_ROLE)
validRound(roundId)
whenNotPaused;
Parameters
roundId
uint32
The ID of the investment round
investor
address
The address of the investor to allocate
ethAmount
uint256
Amount of ETH the investor is allowed to invest
tokenAmount
uint256
Amount of tokens the investor will receive
removeInvestorAllocation
Removes an investor's allocation from a specific investment round
Only callable by MANAGER_ROLE when contract is not paused
Notes:
requires-role: MANAGER_ROLE
security: Ensures investor has no active position before removal
events-emits: {InvestorAllocationRemoved} when allocation is removed
modifiers: validRound, whenNotPaused
throws: InvalidInvestor if investor address is zero
throws: InvalidRoundStatus if round is not in PENDING or ACTIVE status
throws: NoAllocationExists if investor has no allocation
throws: InvestorHasActivePosition if investor has already invested
function removeInvestorAllocation(uint32 roundId, address investor)
external
onlyRole(MANAGER_ROLE)
validRound(roundId)
whenNotPaused;
Parameters
roundId
uint32
The ID of the investment round
investor
address
The address of the investor whose allocation to remove
cancelInvestment
Allows an investor to cancel their investment in an active round
Returns the invested ETH to the investor and updates round state
Notes:
security: Uses nonReentrant guard for ETH transfers
security: Only allows cancellation during active round status
events-emits: {CancelInvestment} when investment is cancelled
modifiers: validRound, activeRound, nonReentrant
throws: NoInvestment if caller has no active investment
throws: InvalidRound if roundId doesn't exist
throws: RoundNotActive if round is not in active status
function cancelInvestment(uint32 roundId) external validRound(roundId) activeRound(roundId) nonReentrant;
Parameters
roundId
uint32
The ID of the investment round
finalizeRound
Finalizes a completed investment round
Deploys vesting contracts and distributes tokens to investors
Notes:
security: Uses nonReentrant guard for token transfers
security: Only callable when contract is not paused
security: Processes all investors in the round
events-emits: {RoundFinalized} when round is finalized
events-emits: {DeployVesting} for each vesting contract created
events-emits: {RoundStatusUpdated} when status changes to FINALIZED
modifiers: validRound, correctStatus(COMPLETED), nonReentrant, whenNotPaused
throws: InvalidRound if roundId doesn't exist
throws: InvalidRoundStatus if round is not in COMPLETED status
function finalizeRound(uint32 roundId)
external
validRound(roundId)
correctStatus(roundId, RoundStatus.COMPLETED)
nonReentrant
whenNotPaused;
Parameters
roundId
uint32
The ID of the investment round to finalize
cancelRound
Cancels an investment round and returns tokens to treasury
Only callable by MANAGER_ROLE when contract is not paused
Notes:
requires-role: MANAGER_ROLE
security: Ensures round can only be cancelled in PENDING or ACTIVE status
security: Automatically returns allocated tokens to treasury
events-emits: {RoundStatusUpdated} when status changes to CANCELLED
events-emits: {RoundCancelled} when round is cancelled
modifiers: validRound, whenNotPaused
throws: InvalidRound if roundId doesn't exist
throws: InvalidRoundStatus if round is not in PENDING or ACTIVE status
function cancelRound(uint32 roundId) external validRound(roundId) onlyRole(MANAGER_ROLE) whenNotPaused;
Parameters
roundId
uint32
The ID of the investment round to cancel
claimRefund
Allows investors to claim ETH refunds from cancelled rounds
Refunds the full invested amount and updates round state
Notes:
security: Uses nonReentrant guard for ETH transfers
security: Ensures round is in CANCELLED status
security: Updates investor position and round state atomically
events-emits: {RefundClaimed} when refund is processed
modifiers: validRound, nonReentrant
throws: InvalidRound if roundId doesn't exist
throws: RoundNotCancelled if round is not in CANCELLED status
throws: NoRefundAvailable if caller has no refund to claim
function claimRefund(uint32 roundId) external validRound(roundId) nonReentrant;
Parameters
roundId
uint32
The ID of the investment round
upgradeTimelockRemaining
Calculates remaining time in the upgrade timelock period
Returns 0 if no upgrade is pending or timelock has expired
Note: security: Helps track upgrade timelock status
function upgradeTimelockRemaining() external view returns (uint256);
Returns
<none>
uint256
uint256 Remaining time in seconds before upgrade can be executed
getRefundAmount
Gets the refundable amount for an investor in a cancelled round
Returns 0 if round is not cancelled or investor has no position
Note: security: Only returns values for cancelled rounds
function getRefundAmount(uint32 roundId, address investor) external view returns (uint256);
Parameters
roundId
uint32
The ID of the investment round
investor
address
The address of the investor to check
Returns
<none>
uint256
uint256 Amount of ETH available for refund
getInvestorDetails
Retrieves investment details for a specific investor in a round
Returns allocation amounts, invested amount, and vesting contract address
function getInvestorDetails(uint32 roundId, address investor)
external
view
returns (uint256 etherAmount, uint256 tokenAmount, uint256 invested, address vestingContract);
Parameters
roundId
uint32
The ID of the investment round to query
investor
address
The address of the investor to query
Returns
etherAmount
uint256
The maximum amount of ETH the investor can invest
tokenAmount
uint256
The amount of tokens allocated to the investor
invested
uint256
The amount of ETH already invested by the investor
vestingContract
address
The address of the investor's vesting contract
getEcosystemToken
Gets the address of the ecosystem token
Provides access to the token contract address
function getEcosystemToken() external view returns (address);
Returns
<none>
address
address The address of the ecosystem token contract
getRoundInfo
Retrieves all information about a specific investment round
Returns the full Round struct with all round parameters
function getRoundInfo(uint32 roundId) external view returns (Round memory);
Parameters
roundId
uint32
The ID of the investment round to query
Returns
<none>
Round
Round Complete round information including status and allocations
getRoundInvestors
Gets the list of investors in a specific round
Returns array of all investor addresses that participated
function getRoundInvestors(uint32 roundId) external view returns (address[] memory);
Parameters
roundId
uint32
The ID of the investment round to query
Returns
<none>
address[]
address[] Array of investor addresses in the round
investEther
Processes ETH investment in a specific investment round
Handles direct investment and fallback function investments
Notes:
security: Uses nonReentrant guard for ETH transfers
security: Validates round status and investor allocation
security: Enforces investment limits and round constraints
events-emits: {Invest} when investment is processed
events-emits: {RoundComplete} if round target is reached
modifiers: validRound, activeRound, whenNotPaused, nonReentrant
throws: RoundEnded if round end time has passed
throws: RoundOversubscribed if maximum investor count reached
throws: NoAllocation if sender has no allocation
throws: AmountAllocationMismatch if sent ETH doesn't match remaining allocation
function investEther(uint32 roundId)
public
payable
validRound(roundId)
activeRound(roundId)
whenNotPaused
nonReentrant;
Parameters
roundId
uint32
The ID of the investment round
getCurrentRound
Gets the ID of the currently active investment round
Iterates through all rounds to find one with ACTIVE status
Notes:
security: Returns max uint32 value as sentinel when no active round is found
view-stability: Does not modify state
function getCurrentRound() public view returns (uint32);
Returns
<none>
uint32
uint32 The ID of the active round, or type(uint32).max if no active round exists
_authorizeUpgrade
Authorizes and executes a contract upgrade after timelock period
Internal function called by the UUPS upgrade mechanism
Notes:
requires-role: UPGRADER_ROLE
security: Enforces timelock period before upgrade execution
security: Verifies upgrade was properly scheduled
security: Checks implementation address matches scheduled upgrade
events-emits: {Upgraded} when upgrade is executed
throws: UpgradeNotScheduled if no upgrade was scheduled
throws: ImplementationMismatch if implementation doesn't match scheduled
throws: UpgradeTimelockActive if timelock period hasn't elapsed
function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE);
Parameters
newImplementation
address
Address of the new implementation contract
_updateRoundStatus
Updates the status of an investment round
Internal function to manage round state transitions
Notes:
security: Ensures status transitions are only forward-moving
events-emits: {RoundStatusUpdated} when status is changed
throws: InvalidStatusTransition if attempting to move to a previous status
function _updateRoundStatus(uint32 roundId, RoundStatus newStatus) internal;
Parameters
roundId
uint32
The ID of the investment round to update
newStatus
RoundStatus
The new status to set for the round
_processInvestment
Processes an investment into a round
Handles investment accounting and status updates
Notes:
security: Updates investor tracking and round state atomically
events-emits: {Invest} when investment is processed
events-emits: {RoundComplete} if round target is reached
function _processInvestment(uint32 roundId, address investor, uint256 amount) private;
Parameters
roundId
uint32
The ID of the investment round
investor
address
The address of the investor
amount
uint256
The amount of ETH being invested
_removeInvestor
Removes an investor from a round's tracking array
Uses optimized removal pattern to maintain array integrity
Note: security: Maintains array consistency with gas-efficient removal
function _removeInvestor(uint32 roundId, address investor) private;
Parameters
roundId
uint32
The ID of the investment round
investor
address
The address of the investor to remove
_deployVestingContract
Deploys a new vesting contract for an investor
Creates and configures a vesting schedule for allocated tokens
Notes:
security: Sets up vesting parameters based on round configuration
events-emits: {DeployVesting} when vesting contract is created
function _deployVestingContract(address investor, uint256 allocation, uint32 roundId) private returns (address);
Parameters
investor
address
The address of the investor who will receive the tokens
allocation
uint256
The amount of tokens to be vested
roundId
uint32
The ID of the investment round
Returns
<none>
address
address The address of the newly deployed vesting contract
Last updated