// SPDX-License-Identifier: UNLICENSED pragma solidity =0.8.18; pragma experimental ABIEncoderV2; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "solowei/contracts/AttoDecimal.sol"; import "solowei/contracts/TwoStageOwnable.sol"; import "../Lockdrop/extensions/TokenRecoveryUpgradeable.sol"; import "../SomaGuard/ISomaGuard.sol"; /** * @title SOMA Starter Contract. * @author SOMA.finance. * @notice A permissioned launchpad contract. */ contract SomaStarter is ReentrancyGuard, TokenRecoveryUpgradeable { using SafeMath for uint256; using SafeERC20 for IERC20; using AttoDecimal for AttoDecimal.Instance; /** * @notice Returns the SomaStarter GLOBAL_ADMIN_ROLE. * @dev Equivalent to keccak256('SomaStarter.Admin') */ bytes32 public constant GLOBAL_ADMIN_ROLE = keccak256("SomaStarter.GLOBAL_ADMIN_ROLE"); /** * @notice Returns the SomaStarter WHITELIST_ROLE. * @dev Equivalent to keccak256('SomaStarter.WHITELIST_ROLE') */ bytes32 public constant GLOBAL_WHITELIST_ROLE = keccak256("SomaStarter.WHITELIST_ROLE"); /** * @notice Returns the SomaStarter LOCAL_ADMIN_ROLE. */ bytes32 public LOCAL_ADMIN_ROLE; /** * @notice Returns the SomaStarter LOCAL_WHITELIST_ROLE */ bytes32 public LOCAL_WHITELIST_ROLE; /** * @notice Pool Type enumeration. */ enum Type { SIMPLE, INTERVAL, LINEAR } /** * @notice Pool Properties structure. * @param issuanceLimit The maximum amount of issuance tokens available. * @param startsAt The timestamp marking the start of the pool. * @param endsAt The timestamp marking the end of the pool. * @param paymentToken The address of the payment token. * @param issuanceToken The address of the issuance token. * @param fee The fee charged on swap. * @param rate The rate between payment token and issuance token. */ struct Props { uint256 issuanceLimit; uint256 startsAt; uint256 endsAt; IERC20 paymentToken; IERC20 issuanceToken; AttoDecimal.Instance fee; AttoDecimal.Instance rate; } /** * @notice Account State structure. * @param limitIndex The limit index of the account. * @param paymentSum The total amount of payment tokens swapped by an account. */ struct AccountState { uint256 limitIndex; uint256 paymentSum; } /** * @notice Complex Account structure. * @param issuanceAmount The amount of issuance token the account is entitled. * @param withdrawnIssuanceAmount The amount of issuance token claimed by the account. */ struct ComplexAccountState { uint256 issuanceAmount; uint256 withdrawnIssuanceAmount; } /** * @notice Account structure. * @param state The Account State structure of the account. * @param complex The Complex Account State structure of the account. * @param immediatelyUnlockedAmount The immediately unlocked amount. * @param unlockedIntervalsCount The number of unlocked intervals that have passed. */ struct Account { AccountState state; ComplexAccountState complex; uint256 immediatelyUnlockedAmount; // linear uint256 unlockedIntervalsCount; // interval } /** * @notice State structure. * @param available The amount of issuance token available. * @param issuance The amount of issuance token added in total. * @param lockedPayments The amount of locked payment tokens. * @param unlockedPayments The amount of unlocked payment tokens. * @param nominatedOwner The address of the nominated owner of the pool. * @param owner The address of the owner of the pool. * @param paymentLimits The array of payment limits for the pool. * @param requiredPrivileges The required privileges of the pool. */ struct State { uint256 available; uint256 issuance; uint256 lockedPayments; uint256 unlockedPayments; address nominatedOwner; address owner; uint256[] paymentLimits; bytes32 requiredPrivileges; } /** * @notice Interval structure. * @param startsAt The timestamp marking the start of the interval. * @param unlockingPart The percentage of the total issuance amount that will be unlocked * in this interval. */ struct Interval { uint256 startsAt; AttoDecimal.Instance unlockingPart; } /** * @notice Linear Properties structure. * @param endsAt The timestamp marking the end of the linear unlock period. * @param duration The length of the linear unlock period in seconds. */ struct LinearProps { uint256 endsAt; uint256 duration; } /** * @notice Pool structure. * @param type_ `Type` enum representing the type of the pool. * @param index The index of the pool. * @param immediatelyUnlockingPart The percentage of total issuance token amount that * immediately unlocked. * @param props The Pool Properties structure of the pool. * @param linear The Linear Properties structure of the pool. * @param state The State structure of the pool. * @param intervals The array of Interval structures of the pool. * @param accounts The mapping of address to Accounts for the pool. */ struct Pool { Type type_; uint256 index; AttoDecimal.Instance immediatelyUnlockingPart; Props props; LinearProps linear; State state; Interval[] intervals; mapping(address => Account) accounts; } Pool[] private _pools; mapping(IERC20 => uint256) private _collectedFees; /** * @notice Modifier to restrict function calls to accounts that have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. */ modifier onlyAdmin() { address sender = _msgSender(); require(hasRole(GLOBAL_ADMIN_ROLE, sender) || hasRole(LOCAL_ADMIN_ROLE, sender), "SomaStarter: ONLY_ADMIN"); _; } /** * @notice Modifier to restrict function calls to accounts that have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. */ modifier onlyWhitelist() { address sender = _msgSender(); require( hasRole(GLOBAL_WHITELIST_ROLE, sender) || hasRole(LOCAL_WHITELIST_ROLE, sender), "SomaStarter: ONLY_WHITELIST" ); _; } /** * @notice Returns the timestamp of the current block. */ function getTimestamp() internal view virtual returns (uint256) { return block.timestamp; } /** * @notice Returns the number of pools. */ function poolsCount() external view returns (uint256) { return _pools.length; } /** * @notice Returns the pool type and properties. * @param poolIndex The index of the pool. * @return type_ The element of the `Type` enum representing the pool type. * @return props The Properties structure of the pool. */ function poolProps(uint256 poolIndex) external view returns (Type type_, Props memory props) { Pool storage pool = _getPool(poolIndex); return (pool.type_, pool.props); } /** * @notice Returns the Interval pool properties, immediately unlocking part and intervals. * @param poolIndex The index of the pool. * @param props The Properties structure of the pool. * @param immediatelyUnlockingPart The immediately unlocking part as a percentage. * @param intervals The array of Interval structures of the pool. */ function intervalPoolProps(uint256 poolIndex) external view returns (Props memory props, AttoDecimal.Instance memory immediatelyUnlockingPart, Interval[] memory intervals) { Pool storage pool = _getPool(poolIndex); _assertPoolIsInterval(pool); return (pool.props, pool.immediatelyUnlockingPart, pool.intervals); } /** * @notice Returns the Linear pool properties, immediately unlocking part and linear unlock details. * @param poolIndex The index of the pool. * @param props The Properties structure of the pool. * @param immediatelyUnlockingPart The immediately unlocking part as a percentage. * @param linear The Linear Properties structure of the pool. */ function linearPoolProps(uint256 poolIndex) external view returns (Props memory props, AttoDecimal.Instance memory immediatelyUnlockingPart, LinearProps memory linear) { Pool storage pool = _getPool(poolIndex); _assertPoolIsLinear(pool); return (pool.props, pool.immediatelyUnlockingPart, pool.linear); } /** * @notice Returns the State structure of the pool. * @param poolIndex The index of the pool. * @param state The State structure of the pool. */ function poolState(uint256 poolIndex) external view returns (State memory state) { return _getPool(poolIndex).state; } /** * @notice Returns the pool type and Account State structure of the pool. * @param poolIndex The index of the pool. * @param address_ The address of the account to return the Account State of. * @return type_ The type of the pool. * @return state The Account State structure of the account. */ function poolAccount(uint256 poolIndex, address address_) external view returns (Type type_, AccountState memory state) { Pool storage pool = _getPool(poolIndex); return (pool.type_, pool.accounts[address_].state); } /** * @notice Returns the Account State, Complex Account state and unlocked interval count of an account and pool. * @param poolIndex The index of the pool. * @param address_ The address of the account to return the state of. * @return state The Account State structure of the account. * @return complex The Complex Account State structure of the account. * @return unlockedIntervalsCount The number of intervals already unlocked. */ function intervalPoolAccount(uint256 poolIndex, address address_) external view returns (AccountState memory state, ComplexAccountState memory complex, uint256 unlockedIntervalsCount) { Pool storage pool = _getPool(poolIndex); _assertPoolIsInterval(pool); Account storage account = pool.accounts[address_]; return (account.state, account.complex, account.unlockedIntervalsCount); } /** * @notice Returns the Account State, Complex Account state and the immediately unlocked amount * of the account and pool. * @param poolIndex The index of the pool. * @param address_ The address of the account to return the state of. * @return state The Account State structure of the account. * @return complex The Complex Account State structure of the account. * @return immediatelyUnlockedAmount The immediately unlocked amount of issuance token. */ function linearPoolAccount(uint256 poolIndex, address address_) external view returns (AccountState memory state, ComplexAccountState memory complex, uint256 immediatelyUnlockedAmount) { Pool storage pool = _getPool(poolIndex); _assertPoolIsLinear(pool); Account storage account = pool.accounts[address_]; return (account.state, account.complex, account.immediatelyUnlockedAmount); } /** * @notice The collected fees of a token. * @param token The token to return the collected fees of. * @return The number of collected tokens from fees. */ function collectedFees(IERC20 token) external view returns (uint256) { return _collectedFees[token]; } /** * @notice Emitted when an account limit is changed. * @param poolIndex The index of the pool. * @param address_ The address of the account to update the account limit of. * @param limitIndex The new limit index of the account. */ event AccountLimitChanged(uint256 indexed poolIndex, address indexed address_, uint256 indexed limitIndex); /** * @notice Emitted when fees are withdrawn. * @param token The address of the token to withdraw as fees. * @param amount The amount of the token to withdraw as fees. */ event FeeWithdrawn(address indexed token, uint256 amount); /** * @notice Emitted when a pool's immediately unlocking part is updated. * @param poolIndex The index of the pool. * @param mantissa The percentage of the overall issuance amount that is designated * for immediate unlock. */ event ImmediatelyUnlockingPartUpdated(uint256 indexed poolIndex, uint256 mantissa); /** * @notice Emitted when a pool interval is created. * @param poolIndex The index of the pool. * @param startsAt The timestamp marking the start of the created interval. * @param unlockingPart The cumulative percentage of the overall issuance token to be * unlocked at the end of this interval. */ event IntervalCreated(uint256 indexed poolIndex, uint256 startsAt, uint256 unlockingPart); /** * @notice Emitted when an account claims issuance tokens from an interval pool. * @param poolIndex The index of the pool. * @param account The address of the account performing the interval unlock. * @param amount The amount of the issuance token unlocked. */ event IntervalPoolUnlocking(uint256 indexed poolIndex, address indexed account, uint256 amount); /** * @notice Emitted when the issuance for a pool is increased. * @param poolIndex The index of the pool. * @param amount The amount of issuance increase. */ event IssuanceIncreased(uint256 indexed poolIndex, uint256 amount); /** * @notice Emitted when a linear pool's unlocking ending timestamp is updated. * @param poolIndex The index of the pool. * @param timestamp The new unlocking ending timestamp. */ event LinearUnlockingEndingTimestampUpdated(uint256 indexed poolIndex, uint256 timestamp); /** * @notice Emitted when an account claims issuance tokens from a linear pool. * @param poolIndex The index of the pool. * @param account The address of the message sender. * @param account The amount of issuance tokens unlocked. */ event LinearPoolUnlocking(uint256 indexed poolIndex, address indexed account, uint256 amount); /** * @notice Emitted when a payment limit is created for a pool. * @param poolIndex The index of the pool. * @param limitIndex The new limit index. * @param limit The payment limit associated with the new limit index. */ event PaymentLimitCreated(uint256 indexed poolIndex, uint256 indexed limitIndex, uint256 limit); /** * @notice Emitted when a payment limit is updated. * @param poolIndex The index of the pool. * @param limitIndex The new limit index. * @param newLimit The new payment limit associated with the limit index. */ event PaymentLimitChanged(uint256 indexed poolIndex, uint256 indexed limitIndex, uint256 newLimit); /** * @notice Emitted when a payments are withdrawn by an admin account. * @param poolIndex The index of the pool. * @param unlockedAmount The amount of payment token unlocked. * @param collectedFee The amount of collected fees denominated in the payment token. */ event PaymentUnlocked(uint256 indexed poolIndex, uint256 unlockedAmount, uint256 collectedFee); /** * @notice Emitted when a payments are withdrawn by an admin account. * @param poolIndex The index of the pool. * @param amount The amount of payment token withdrawn. */ event PaymentsWithdrawn(uint256 indexed poolIndex, uint256 amount); /** * @notice Emitted when a pool owner is changed. * @param poolIndex The index of the pool. * @param newOwner The address of the new pool owner. */ event PoolOwnerChanged(uint256 indexed poolIndex, address indexed newOwner); /** * @notice Emitted when a new pool owner is nominated. * @param poolIndex The index of the pool. * @param nominatedOwner The address of the new nominated owner. */ event PoolOwnerNominated(uint256 indexed poolIndex, address indexed nominatedOwner); /** * @notice Emitted when unsold issuance tokens are withdrawn from a pool. * @param poolIndex The index of the pool. * @param amount The amount of unsold issuance tokens to withdraw. */ event UnsoldWithdrawn(uint256 indexed poolIndex, uint256 amount); /** * @notice Emitted when a pool is created. * @param type_ The type of the pool, represented in one of the `Type` enum elements. * @param paymentToken The address of the pool's payment token. * @param issuanceToken The address of the pool's issuance token. * @param poolIndex The index of the pool. * @param issuanceLimit The maximum issuance amount for the pool. * @param startsAt The timestamp marking the start of the pool. * @param endsAt The timestamp marking the end of the pool. * @param fee The fee as a percentage to charge accounts upon swap. * @param rate The rate as a percentage between the payment token and issuance token. * @param paymentLimit The maximum amount a user can swap denominated in the payment token. */ event PoolCreated( Type type_, IERC20 indexed paymentToken, IERC20 indexed issuanceToken, uint256 poolIndex, uint256 issuanceLimit, uint256 startsAt, uint256 endsAt, uint256 fee, uint256 rate, uint256 paymentLimit ); /** * @notice Emitted when pool privileges are updated. * @param poolIndex The index of the pool. * @param newRequiredPrivileges The new required privileges of the pool. */ event PoolPrivilegesChanged(uint256 poolIndex, bytes32 newRequiredPrivileges); /** * @notice Emitted when an account calls swap. * @param poolIndex The index of the pool. * @param caller The address of the function caller. * @param requestedPaymentAmount The requested payment amount by the account. * @param paymentAmount The actual payment amount paid by the account. * @param issuanceAmount The amount of issuance token received by the caller. */ event Swap( uint256 indexed poolIndex, address indexed caller, uint256 requestedPaymentAmount, uint256 paymentAmount, uint256 issuanceAmount ); /** * @notice Initializer for the contract. */ function initialize() external initializer { __TokenRecovery__init(); LOCAL_ADMIN_ROLE = keccak256(abi.encodePacked(address(this), GLOBAL_ADMIN_ROLE)); LOCAL_WHITELIST_ROLE = keccak256(abi.encodePacked(address(this), GLOBAL_WHITELIST_ROLE)); } /** * @notice Creates a pool of Simple type. * @param props The pool properties of the new pool. * @param paymentLimit The initial payment limit of the pool. * @param requiredPrivileges The required privileges of the pool. * @param owner_ The address of the owner of the pool. * @custom:emits PoolCreated * @custom:emits PoolOwnerChanged * @custom:requirement The fee in `props` must be a valid percentage. * @custom:requirement The `startsAt` must be less than the `endsAt` in `props`. * @custom:requirement The message sender must have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. * @return success Boolean indicating if the transaction was successful. * @return poolIndex The index of the newly created pool. */ function createSimplePool(Props memory props, uint256 paymentLimit, bytes32 requiredPrivileges, address owner_) external onlyAdmin returns (bool success, uint256 poolIndex) { poolIndex = _createSimplePool(props, paymentLimit, owner_, Type.SIMPLE).index; _updateRequiredPrivileges(poolIndex, requiredPrivileges); return (true, poolIndex); } /** * @notice Creates a pool of Interval type. * @param props The pool properties of the new pool. * @param paymentLimit The initial payment limit of the pool. * @param requiredPrivileges The required privileges of the pool. * @param owner_ The address of the owner of the pool. * @param immediatelyUnlockingPart The immediately unlocking part as a percent of overall issuance token. * @param intervals The array of Interval structures representing the intervals of the pool. * @custom:emits PoolCreated * @custom:emits PoolOwnerChanged * @custom:emits IntervalCreated * @custom:emits ImmediatelyUnlockingPartUpdated * @custom:requirement The last `immediatelyUnlockingPart` must be less than `1`. * @custom:requirement The `unlockingPart` in each interval in `intervals` must be greater than the last unlocking part * in the array. * @custom:requirement The `startsAt` in each interval in `intervals` must be greater than the last starts at timestamp * in the array. * @custom:requirement The last `unlockingPart` in `intervals` must be equal to `1`. * @custom:requirement The message sender must have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. * @return success Boolean indicating if the transaction was successful. * @return poolIndex The index of the newly created pool. */ function createIntervalPool( Props memory props, uint256 paymentLimit, bytes32 requiredPrivileges, address owner_, AttoDecimal.Instance memory immediatelyUnlockingPart, Interval[] memory intervals ) external onlyAdmin returns (bool success, uint256 poolIndex) { Pool storage pool = _createSimplePool(props, paymentLimit, owner_, Type.INTERVAL); _updateRequiredPrivileges(pool.index, requiredPrivileges); _setImmediatelyUnlockingPart(pool, immediatelyUnlockingPart); uint256 intervalsCount = intervals.length; AttoDecimal.Instance memory lastUnlockingPart = immediatelyUnlockingPart; uint256 lastIntervalStartingTimestamp = props.endsAt - 1; for (uint256 i = 0; i < intervalsCount; i++) { Interval memory interval = intervals[i]; require(interval.unlockingPart.gt(lastUnlockingPart), "SomaStarter: INVALID_INTERVAL_UNLOCKING_PART"); lastUnlockingPart = interval.unlockingPart; uint256 startingTimestamp = interval.startsAt; require( startingTimestamp > lastIntervalStartingTimestamp, "SomaStarter: INVALID_INTERVAL_STARTING_TIMESTAMP" ); lastIntervalStartingTimestamp = startingTimestamp; pool.intervals.push(interval); emit IntervalCreated(poolIndex, interval.startsAt, interval.unlockingPart.mantissa); } require(lastUnlockingPart.eq(1), "SomaStarter: UNLOCKING_PART_NOT_EQUAL_ONE"); return (true, pool.index); } /** * @notice Creates a pool of Linear type. * @param props The pool properties of the new pool. * @param paymentLimit The initial payment limit of the pool. * @param requiredPrivileges The required privileges of the pool. * @param owner_ The address of the owner of the pool. * @param immediatelyUnlockingPart The immediately unlocking part as a percent of overall issuance token. * @param linearUnlockingEndsAt The array of Interval structures representing the intervals of the pool. * @custom:emits PoolCreated * @custom:emits PoolOwnerChanged * @custom:emits LinearUnlockingEndingTimestampUpdated * @custom:emits ImmediatelyUnlockingPartUpdated * @custom:requirement The `linearUnlockingEndsAt` must be greater than the ``props``' `endsAt` timestamp. * @custom:requirement The `immediatelyUnlockingPart` must be less than `1`. * @custom:requirement The message sender must have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. * @return success Boolean indicating if the transaction was successful. * @return poolIndex The index of the newly created pool. */ function createLinearPool( Props memory props, uint256 paymentLimit, bytes32 requiredPrivileges, address owner_, AttoDecimal.Instance memory immediatelyUnlockingPart, uint256 linearUnlockingEndsAt ) external onlyAdmin returns (bool success, uint256 poolIndex) { require(linearUnlockingEndsAt > props.endsAt, "SomaStarter: LINEAR_POOL_NOT_ENDED"); Pool storage pool = _createSimplePool(props, paymentLimit, owner_, Type.LINEAR); _updateRequiredPrivileges(pool.index, requiredPrivileges); _setImmediatelyUnlockingPart(pool, immediatelyUnlockingPart); pool.linear.endsAt = linearUnlockingEndsAt; pool.linear.duration = linearUnlockingEndsAt - props.endsAt; emit LinearUnlockingEndingTimestampUpdated(pool.index, linearUnlockingEndsAt); return (true, pool.index); } /** * @notice Increases the issuance amount for a pool. * @param poolIndex The index of the pool. * @param amount The amount of issuance token to increase. * @custom:emits IssuanceIncreased * @custom:requirement `amount` must be greater than zero. * @custom:requirement The timestamp of the function call must be less than ``props``' `endsAt`. * @custom:requirement The function caller must be the owner of the pool. * @custom:requirement The new total issuance amount of the pool must be less than or equal to ``props``' `issuanceLimit`. * @return success Boolean indicating if the transaction was successful. */ function increaseIssuance(uint256 poolIndex, uint256 amount) external returns (bool success) { require(amount > 0, "SomaStarter: ZERO_AMOUNT"); Pool storage pool = _getPool(poolIndex); require(getTimestamp() < pool.props.endsAt, "SomaStarter: POOL_ENDED"); address caller = msg.sender; _assertPoolOwnership(pool, caller); pool.state.issuance = pool.state.issuance.add(amount); require(pool.state.issuance <= pool.props.issuanceLimit, "SomaStarter: ISSUANCE_LIMIT_EXCEEDED"); pool.state.available = pool.state.available.add(amount); emit IssuanceIncreased(poolIndex, amount); pool.props.issuanceToken.safeTransferFrom(caller, address(this), amount); return true; } /** * @notice Swaps a pool's payment token for the issuance token. * @param poolIndex The index of the pool. * @param requestedPaymentAmount The requested payment amount by the message sender. * @custom:emits Swap * @custom:requirement `amount` must be greater than zero. * @custom:requirement `requestedPaymentAmount` must be greater than zero. * @custom:requirement The timestamp of the function call must be greater than or equal to the pool start time. * @custom:requirement The timestamp of the function call must be less than the pool end time. * @custom:requirement The pool must have a positive `available` issuance amount. * @custom:requirement The message sender's account state's `paymentSum` must be less than the account state's `paymentLimit`. * @custom:requirement The message sender's account must have the required privileges of the pool. * @return paymentAmount The amount of payment token swapped by the account. * @return issuanceAmount The amount of issuance token received by the account. */ function swap(uint256 poolIndex, uint256 requestedPaymentAmount) external nonReentrant returns (uint256 paymentAmount, uint256 issuanceAmount) { require(requestedPaymentAmount > 0, "SomaStarter: REQUESTED_PAYMENT_AMOUNT_ZERO"); address caller = msg.sender; ISomaGuard guard = ISomaGuard(SOMA.guard()); Pool storage pool = _getPool(poolIndex); uint256 timestamp = getTimestamp(); require(timestamp >= pool.props.startsAt, "SomaStarter: POOL_NOT_STARTED"); require(timestamp < pool.props.endsAt, "SomaStarter: POOL_ENDED"); require(pool.state.available > 0, "SomaStarter: NO_AVAILABLE_ISSUANCE"); require(guard.check(caller, pool.state.requiredPrivileges), "SomaStarter: NO_PRIVILEGES"); (paymentAmount, issuanceAmount) = _calculateSwapAmounts(pool, requestedPaymentAmount, caller); Account storage account = pool.accounts[caller]; if (paymentAmount > 0) { pool.state.lockedPayments = pool.state.lockedPayments.add(paymentAmount); account.state.paymentSum = account.state.paymentSum.add(paymentAmount); pool.props.paymentToken.safeTransferFrom(caller, address(this), paymentAmount); } if (issuanceAmount > 0) { if (pool.type_ == Type.SIMPLE) { pool.props.issuanceToken.safeTransfer(caller, issuanceAmount); } else { uint256 totalIssuanceAmount = account.complex.issuanceAmount.add(issuanceAmount); account.complex.issuanceAmount = totalIssuanceAmount; uint256 newWithdrawnIssuanceAmount = pool.immediatelyUnlockingPart.mul(totalIssuanceAmount).floor(); uint256 issuanceToWithdraw = newWithdrawnIssuanceAmount - account.complex.withdrawnIssuanceAmount; account.complex.withdrawnIssuanceAmount = newWithdrawnIssuanceAmount; if (pool.type_ == Type.LINEAR) account.immediatelyUnlockedAmount = newWithdrawnIssuanceAmount; if (issuanceToWithdraw > 0) pool.props.issuanceToken.safeTransfer(caller, issuanceToWithdraw); } pool.state.available = pool.state.available.sub(issuanceAmount); } emit Swap(poolIndex, caller, requestedPaymentAmount, paymentAmount, issuanceAmount); } /** * @notice Withdraws an amount of issuance tokens from an interval pool. * @param poolIndex The index of the pool. * @param intervalIndex The index of the interval to be unlocked. * @custom:emits IntervalPoolUnlocking * @custom:requirement The pool must be of interval type. * @custom:requirement The `intervalIndex` must be less than the length of the pool's intervals array. * @custom:requirement The timestamp of the function caller must be greater than or equal to the interval start time. * @return withdrawnIssuanceAmount The amount of issuance tokens withdrawn. */ function unlockInterval(uint256 poolIndex, uint256 intervalIndex) external returns (uint256 withdrawnIssuanceAmount) { address caller = msg.sender; Pool storage pool = _getPool(poolIndex); _assertPoolIsInterval(pool); require(intervalIndex < pool.intervals.length, "SomaStarter: INVALID_INTERVAL_INDEX"); Interval storage interval = pool.intervals[intervalIndex]; require(interval.startsAt <= getTimestamp(), "SomaStarter: INTERVAL_NOT_STARTED"); Account storage account = pool.accounts[caller]; require(intervalIndex >= account.unlockedIntervalsCount, "SomaStarter: INTERVAL_ALREADY_UNLOCKED"); uint256 newWithdrawnIssuanceAmount = interval.unlockingPart.mul(account.complex.issuanceAmount).floor(); uint256 issuanceToWithdraw = newWithdrawnIssuanceAmount - account.complex.withdrawnIssuanceAmount; account.complex.withdrawnIssuanceAmount = newWithdrawnIssuanceAmount; if (issuanceToWithdraw > 0) { pool.props.issuanceToken.safeTransfer(caller, issuanceToWithdraw); emit IntervalPoolUnlocking(pool.index, caller, issuanceToWithdraw); } account.unlockedIntervalsCount = intervalIndex.add(1); return issuanceToWithdraw; } /** * @notice Withdraws an amount of issuance tokens for a linear pool. * @param poolIndex The index of the pool. * @custom:emits LinearPoolUnlocking * @custom:requirement The pool must be of linear type. * @custom:requirement The timestamp of the function call must be greater than or equal to the pool end time. * @custom:requirement The message sender's account complex `withdrawnIssuanceAmount` must be less than the account complex's * `issuanceAmount`. * @return withdrawalAmount The amount of issuance tokens withdrawn. */ function unlockLinear(uint256 poolIndex) external returns (uint256 withdrawalAmount) { address caller = msg.sender; uint256 timestamp = getTimestamp(); Pool storage pool = _getPool(poolIndex); _assertPoolIsLinear(pool); require(pool.props.endsAt < timestamp, "SomaStarter: POOL_NOT_ENDED"); Account storage account = pool.accounts[caller]; uint256 issuanceAmount = account.complex.issuanceAmount; require(account.complex.withdrawnIssuanceAmount < issuanceAmount, "SomaStarter: ALL_FUNDS_UNLOCKED"); uint256 passedTime = timestamp - pool.props.endsAt; uint256 freezedAmount = issuanceAmount.sub(account.immediatelyUnlockedAmount); uint256 unfreezedAmount = passedTime.mul(freezedAmount).div(pool.linear.duration); uint256 newWithdrawnIssuanceAmount = timestamp >= pool.linear.endsAt ? issuanceAmount : Math.min(account.immediatelyUnlockedAmount.add(unfreezedAmount), issuanceAmount); withdrawalAmount = newWithdrawnIssuanceAmount.sub(account.complex.withdrawnIssuanceAmount); if (withdrawalAmount > 0) { account.complex.withdrawnIssuanceAmount = newWithdrawnIssuanceAmount; emit LinearPoolUnlocking(pool.index, caller, withdrawalAmount); pool.props.issuanceToken.safeTransfer(caller, withdrawalAmount); } } /** * @notice Creates a payment limit for a pool. * @param poolIndex The index of the pool. * @param limit The payment limit denominated in the pool's payment token. * @custom:emits PaymentLimitCreated * @custom:requirement The function caller must be the owner of the pool. * @return limitIndex The index of the payment limit. */ function createPaymentLimit(uint256 poolIndex, uint256 limit) external onlyWhitelist returns (uint256 limitIndex) { Pool storage pool = _getPool(poolIndex); limitIndex = pool.state.paymentLimits.length; pool.state.paymentLimits.push(limit); emit PaymentLimitCreated(poolIndex, limitIndex, limit); } /** * @notice Updates a limit index's limit amount for a pool. * @param poolIndex The index of the pool. * @param limitIndex The limit index to update the limit for. * @param newLimit The updated limit for the limit index. * @custom:emits PaymentLimitChanged * @custom:requirement The function caller must be the owner of the pool. * @custom:requirement The `limitIndex` must be less than the length of the pool state's paymentLimit. * @return success Boolean indicating if the transaction was successful. */ function changeLimit(uint256 poolIndex, uint256 limitIndex, uint256 newLimit) external onlyWhitelist returns (bool success) { Pool storage pool = _getPool(poolIndex); _validateLimitIndex(pool, limitIndex); pool.state.paymentLimits[limitIndex] = newLimit; emit PaymentLimitChanged(poolIndex, limitIndex, newLimit); return true; } /** * @notice Sets an account's limit for a pool. * @param poolIndex The index of the pool. * @param limitIndex The limit index of the limit to assign to the account. * @param accounts The array of accounts to assign to the limit. * @custom:emits AccountLimitChanged * @custom:requirement The length of `accounts` must be greater than zero. * @custom:requirement The function caller must be the owner of the pool. * @custom:requirement The `limitIndex` must be less than the length of the pool state's paymentLimit. * @return success Boolean indicating if the transaction was successful. */ function setAccountsLimit(uint256 poolIndex, uint256 limitIndex, address[] memory accounts) external onlyWhitelist returns (bool success) { Pool storage pool = _getPool(poolIndex); _validateLimitIndex(pool, limitIndex); uint256 accountsCount = accounts.length; require(accountsCount > 0, "SomaStarter: NO_ACCOUNTS_PROVIDED"); for (uint256 i = 0; i < accountsCount; i++) { address account = accounts[i]; Account storage poolAccount_ = pool.accounts[account]; if (poolAccount_.state.limitIndex == limitIndex) continue; poolAccount_.state.limitIndex = limitIndex; emit AccountLimitChanged(poolIndex, account, limitIndex); } return true; } /** * @notice Withdraws payment tokens from a pool. * @param poolIndex The index of the pool. * @custom:emits PaymentsWithdrawn * @custom:emits PaymentUnlocked * @custom:requirement The function caller must be the owner of the pool. * @custom:requirement The pool state's unlockedPayments must be greater than zero. * @return success Boolean indicating if the transaction was successful. */ function withdrawPayments(uint256 poolIndex) external returns (bool success) { Pool storage pool = _getPool(poolIndex); address caller = msg.sender; _assertPoolOwnership(pool, caller); _unlockPayments(pool); uint256 collectedPayments = pool.state.unlockedPayments; require(collectedPayments > 0, "SomaStarter: NO_COLLECTED_PAYMENTS"); pool.state.unlockedPayments = 0; emit PaymentsWithdrawn(poolIndex, collectedPayments); pool.props.paymentToken.safeTransfer(caller, collectedPayments); return true; } /** * @notice Withdraws unsold issuance tokens from the pool. * @param poolIndex The index of the pool. * @custom:emits UnsoldWithdrawn * @custom:requirement The function caller must be the owner of the pool. * @custom:requirement The timestamp of the function call must be greater than or equal to the pool ``props``' endsAt. * @custom:requirement The pool ``state``'s `available` must be greater than zero. * @return success Boolean indicating if the transaction was successful. */ function withdrawUnsold(uint256 poolIndex) external returns (bool success) { Pool storage pool = _getPool(poolIndex); address caller = msg.sender; _assertPoolOwnership(pool, caller); require(getTimestamp() >= pool.props.endsAt, "SomaStarter: NOT_ENDED"); uint256 amount = pool.state.available; require(amount > 0, "SomaStarter: NO_UNSOLD"); pool.state.available = 0; emit UnsoldWithdrawn(poolIndex, amount); pool.props.issuanceToken.safeTransfer(caller, amount); return true; } /** * @notice Collects fees from a pool. * @param poolIndex The index of the pool. * @custom:emits PaymentUnlocked * @custom:requirement The function caller must have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. * @custom:requirement The `poolIndex` must be less than the total number of pools. * @return success Boolean indicating if the transaction was successful. */ function collectFee(uint256 poolIndex) external onlyAdmin returns (bool success) { _unlockPayments(_getPool(poolIndex)); return true; } /** * @notice Withdraws fees from a pool. * @param token The address of the token to withdraw as fees. * @param to The address to send the fees to. * @custom:emits FeeWithdrawn * @custom:requirement The collected fees must be greater than zero. * @custom:requirement The function caller must have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. * @return success Boolean indicating if the transaction was successful. */ function withdrawFee(IERC20 token, address to) external onlyAdmin returns (bool success) { uint256 collectedFee = _collectedFees[token]; require(collectedFee > 0, "SomaStarter: NO_COLLECTED_FEES"); _collectedFees[token] = 0; emit FeeWithdrawn(address(token), collectedFee); token.safeTransfer(to, collectedFee); return true; } /** * @notice Nominates a new pool owner. * @param poolIndex The index of the pool. * @param nominatedOwner_ The new nominated owner of the pool. * @custom:emits PoolOwnerNominated * @custom:requirement The function caller must be the existing pool owner. * @custom:requirement The new nominated pool owner must not be equal to the existing pool owner. * @return success Boolean indicating if the transaction was successful. */ function nominateNewPoolOwner(uint256 poolIndex, address nominatedOwner_) external returns (bool success) { Pool storage pool = _getPool(poolIndex); _assertPoolOwnership(pool, msg.sender); require(nominatedOwner_ != pool.state.owner, "SomaStarter: ALREADY_OWNER"); if (pool.state.nominatedOwner == nominatedOwner_) return true; pool.state.nominatedOwner = nominatedOwner_; emit PoolOwnerNominated(poolIndex, nominatedOwner_); return true; } /** * @notice Accepts ownership of a pool. * @param poolIndex The index of the pool. * @custom:emits PoolOwnerChanged * @custom:requirement The function caller must be the pool ``state``'s nominated owner. * @return success Boolean indicating if the transaction was successful. */ function acceptPoolOwnership(uint256 poolIndex) external returns (bool success) { Pool storage pool = _getPool(poolIndex); address caller = msg.sender; require(pool.state.nominatedOwner == caller, "SomaStarter: NOT_NOMINATED_POOL_OWNERSHIP"); pool.state.owner = caller; pool.state.nominatedOwner = address(0); emit PoolOwnerChanged(poolIndex, caller); return true; } /** * @notice Updates the required privileges of the pool. * @param poolIndex The index of the pool. * @param newRequiredPrivileges The new required privileges of the pool. * @custom:emits PoolPrivilegesChanged * @custom:requirement The function caller must have the GLOBAL_ADMIN_ROLE or LOCAL_ADMIN_ROLE. * @return success Boolean indicating if the transaction was successful. */ function updateRequiredPrivileges(uint256 poolIndex, bytes32 newRequiredPrivileges) external onlyAdmin returns (bool success) { _updateRequiredPrivileges(poolIndex, newRequiredPrivileges); return true; } function _assertPoolIsInterval(Pool storage pool) private view { require(pool.type_ == Type.INTERVAL, "SomaStarter: NOT_INTERVAL_POOL"); } function _assertPoolIsLinear(Pool storage pool) private view { require(pool.type_ == Type.LINEAR, "SomaStarter: NOT_LINEAR_POOL"); } function _assertPoolOwnership(Pool storage pool, address account) private view { require(account == pool.state.owner, "SomaStarter: PERMISSION_DENIED"); } function _calculateSwapAmounts(Pool storage pool, uint256 requestedPaymentAmount, address account) private view returns (uint256 paymentAmount, uint256 issuanceAmount) { paymentAmount = requestedPaymentAmount; Account storage poolAccount_ = pool.accounts[account]; uint256 paymentLimit = pool.state.paymentLimits[poolAccount_.state.limitIndex]; require(poolAccount_.state.paymentSum < paymentLimit, "SomaStarter: ACCOUNT_PAYMENT_LIMIT_EXCEEDED"); if (poolAccount_.state.paymentSum.add(paymentAmount) > paymentLimit) { paymentAmount = paymentLimit.sub(poolAccount_.state.paymentSum); } issuanceAmount = pool.props.rate.mul(paymentAmount).floor(); if (issuanceAmount > pool.state.available) { issuanceAmount = pool.state.available; paymentAmount = AttoDecimal.div(issuanceAmount, pool.props.rate).ceil(); } } function _getPool(uint256 index) private view returns (Pool storage) { require(index < _pools.length, "SomaStarter: INVALID_POOL_INDEX"); return _pools[index]; } function _validateLimitIndex(Pool storage pool, uint256 limitIndex) private view { require(limitIndex < pool.state.paymentLimits.length, "SomaStarter: INVALID_LIMIT_INDEX"); } function _createSimplePool(Props memory props, uint256 paymentLimit, address owner_, Type type_) private returns (Pool storage) { { uint256 timestamp = getTimestamp(); if (props.startsAt < timestamp) props.startsAt = timestamp; require(props.fee.lt(1), "SomaStarter: INVALID_PERCENT"); require(props.startsAt < props.endsAt, "SomaStarter: INVALID_ENDING_TIMESTAMP"); } uint256 poolIndex = _pools.length; _pools.push(); Pool storage pool = _pools[poolIndex]; pool.index = poolIndex; pool.type_ = type_; pool.props = props; pool.state.paymentLimits = new uint256[](1); pool.state.paymentLimits[0] = paymentLimit; pool.state.owner = owner_; emit PoolCreated( type_, props.paymentToken, props.issuanceToken, poolIndex, props.issuanceLimit, props.startsAt, props.endsAt, props.fee.mantissa, props.rate.mantissa, paymentLimit ); emit PoolOwnerChanged(poolIndex, owner_); return pool; } function _setImmediatelyUnlockingPart(Pool storage pool, AttoDecimal.Instance memory immediatelyUnlockingPart) private { require(immediatelyUnlockingPart.lt(1), "SomaStarter: INVALID_IMMEDIATELY_UNLOCKING_PART"); pool.immediatelyUnlockingPart = immediatelyUnlockingPart; emit ImmediatelyUnlockingPartUpdated(pool.index, immediatelyUnlockingPart.mantissa); } function _unlockPayments(Pool storage pool) private { if (pool.state.lockedPayments == 0) return; uint256 fee = pool.props.fee.mul(pool.state.lockedPayments).ceil(); _collectedFees[pool.props.paymentToken] = _collectedFees[pool.props.paymentToken].add(fee); uint256 unlockedAmount = pool.state.lockedPayments.sub(fee); pool.state.unlockedPayments = pool.state.unlockedPayments.add(unlockedAmount); pool.state.lockedPayments = 0; emit PaymentUnlocked(pool.index, unlockedAmount, fee); } function _updateRequiredPrivileges(uint256 poolIndex, bytes32 newRequiredPrivileges) private { emit PoolPrivilegesChanged(poolIndex, newRequiredPrivileges); Pool storage pool = _getPool(poolIndex); pool.state.requiredPrivileges = newRequiredPrivileges; } }