diff --git a/classDiagram.svg b/classDiagram.svg index f467820..47e8cf9 100644 --- a/classDiagram.svg +++ b/classDiagram.svg @@ -132,7 +132,7 @@   _paymentSplitter: CustomPaymentSplitter   _cumReceivedInvestments: uint256   _tiers: Tier[] -   _helper: DecentralisedInvestmentHelper +   _helper: Helper   _tierInvestments: TierInvestment[] Private: @@ -177,7 +177,7 @@ 5 -DecentralisedInvestmentHelper +Helper src/Helper.sol Public: @@ -187,7 +187,7 @@    isInRange(minVal: uint256, maxVal: uint256, someVal: uint256): (inRange: bool)    computeCurrentInvestmentTier(cumReceivedInvestments: uint256, tiers: Tier[]): (currentTier: Tier)    getRemainingAmountInCurrentTier(cumReceivedInvestments: uint256, someTier: Tier): (remainingAmountInTier: uint256) -    computeRemainingInvestorPayout(cumRemainingInvestorReturn: uint256, investorFracNumerator: uint256, investorFracDenominator: uint256, paidAmount: uint256): (returneCumRemainingInvestorReturn: uint256) +    computeRemainingInvestorPayout(cumRemainingInvestorReturn: uint256, investorFracNumerator: uint256, investorFracDenominator: uint256, paidAmount: uint256): (returnCumRemainingInvestorReturn: uint256) @@ -246,7 +246,7 @@     hasReachedInvestmentCeiling(cumReceivedInvestments: uint256, tiers: Tier[]): (reachedInvestmentCeiling: bool)     computeCurrentInvestmentTier(cumReceivedInvestments: uint256, tiers: Tier[]): (currentTier: Tier)     getRemainingAmountInCurrentTier(cumReceivedInvestments: uint256, someTier: Tier): (remainingAmountInTier: uint256) -     computeRemainingInvestorPayout(cumRemainingInvestorReturn: uint256, investorFracNumerator: uint256, investorFracDenominator: uint256, paidAmount: uint256): (returneCumRemainingInvestorReturn: uint256) +     computeRemainingInvestorPayout(cumRemainingInvestorReturn: uint256, investorFracNumerator: uint256, investorFracDenominator: uint256, paidAmount: uint256): (returnCumRemainingInvestorReturn: uint256) diff --git a/src/CustomPaymentSplitter.sol b/src/CustomPaymentSplitter.sol index 3e7a245..e41414a 100644 --- a/src/CustomPaymentSplitter.sol +++ b/src/CustomPaymentSplitter.sol @@ -50,8 +50,35 @@ contract CustomPaymentSplitter is Interface { } /** - * @dev Constructor - */ + @notice This constructor initializes the `CustomPaymentSplitter` contract. + + @dev This constructor performs the following actions: + + 1. Validates that the provided lists of payees and corresponding amounts owed have + the same length. It ensures at least one payee is specified. That implicitly + veries that at least one amountsOwed element is given. + + 2. Sets the contract owner to the message sender (`msg.sender`). This contract + is designed to be initialised by the DecentralisedInvestmentManager contract. + + 3. Stores the provided `amountsOwed` array in the internal `_amountsOwed` + variable. + + 4. Iterates through the `payees` and `amountsOwed` arrays, calling the + `_addPayee` internal function for each element to register payees and their + initial shares. + + **Important Notes:** + + * The `CustomPaymentSplitter` contract is designed for splitting payments among + multiple payees based on predefined shares. It is a modificiation of the + PaymentSplitter contract by OpenZeppelin. + + @param payees A list of wallet addresses representing the people that can + receive money. + @param amountsOwed A list of WEI amounts representing the initial shares + allocated to each payee. + */ constructor(address[] memory payees, uint256[] memory amountsOwed) public payable { require(payees.length == amountsOwed.length, "The nr of payees is not equal to the nr of amounts owed."); require(payees.length > 0, "There are not more than 0 payees."); @@ -66,18 +93,40 @@ contract CustomPaymentSplitter is Interface { } /** - Doubt: by not requiring msg.sender == account, one allows anyone to trigger - the release of the investment funds. This can be inefficient for tax - purposes. - * @dev Release one of the payee's proportional payment. - * @param account Whose payments will be released. - */ + @notice This function allows a payee to claim their outstanding wei balance. + + @dev This function is designed to be called by payees to withdraw their share of + collected DAI. It performs the following actions: + + 1. Validates that the payee's outstanding wei balance (the difference between + their total nr of "shares" and any previous releases) is greater than zero. + + 2. Calculates the amount to be paid to the payee by subtracting any previously + released wei from their initial share. + + 3. Verifies that the calculated payment amount is greater than zero. + + 4. Updates the internal accounting for the payee's released wei and the total + contract-wide released wei. + + 5. Transfers the calculated payment amount of wei to the payee's address using + a secure `transfer` approach. + + 6. Emits a `PaymentReleased` event to log the payment details. + + **Important Notes:** + * Payees are responsible for calling this function to claim their outstanding + balances. + + @param account The address of the payee requesting a release. + + + */ function release(address payable account) public override { require(_dai[account] > 0, "The dai for account, was not larger than 0."); // The amount the payee may receive is equal to the amount of outstanding // DAI, subtracted by the amount that has been released to that account. - uint256 payment = _dai[account] - _released[account]; require(payment > 0, "The amount to be paid was not larger than 0."); @@ -112,33 +161,93 @@ contract CustomPaymentSplitter is Interface { } /** - * Public counterpart of the _addPayee function, to add users that can withdraw - * funds after constructor initialisation. - */ + @notice This function allows the contract owner to add additional "shares" to an existing payee. + + @dev This function increases the "share" allocation of a registered payee. It performs + the following actions: + + 1. Validates that the additional share amount (in WEI) is greater than zero. + + 2. Verifies that the payee address already exists in the `_dai` mapping (implicit + through requirement check). + + 3. Updates the payee's share balance in the `_dai` mapping by adding the provided + `dai` amount. + + 4. Updates the contract-wide total DAI amount by adding the provided `dai` amount. + + 5. Emits a `SharesAdded` event to log the details of the share increase. + + **Important Notes:** + + * This function can only be called by the contract owner _dim. It cannot be + called by the projectLead. + * The payee must already be registered with the contract to receive additional + shares. + + @param account The address of the payee to receive additional shares. + @param dai The amount of additional DAI shares to be allocated (in WEI). + + */ function publicAddSharesToPayee(address account, uint256 dai) public override onlyOwner { require(dai > 0, "There were 0 dai shares incoming."); - // TODO: assert account is in _dai array. - + // One can not assert the account is already in _dai, because inherently in + // Solidity, a mapping contains all possible options already. So it will + // always return true. Furthermore, all values are initialised at 0 for + // this mapping, which also is a valid value for an account that is + // already in there. _dai[account] = _dai[account] + dai; _totalDai = _totalDai + dai; emit SharesAdded(account, dai); } - // This function can receive Ether from other accounts + /** + @notice This function is used to deposit funds into the `CustomPaymentSplitter` + contract. + + @dev This function allows anyone to deposit funds into the contract. It primarily + serves as a way to collect investment funds or other revenue streams. The function + logs the deposit details by emitting a `PaymentReceived` event. + + **Important Notes:** + + * There is no restriction on who can call this function. + * TODO: Consider implementing access control mechanisms if only specific addresses + should be allowed to deposit funds. This may be important because some + business logic/balance checks may malfunction if unintentional funds come in. + + */ function deposit() public payable override { // Event to log deposits emit PaymentReceived(msg.sender, msg.value); } /** - * return the amount already released to an account. - */ + @notice This function retrieves the total amount of wei that has already been released to a specific payee. + + @dev This function is a view function, meaning it doesn't modify the contract's state. It returns the accumulated + amount of wei that has been released to the provided payee address. + + @param account The address of the payee for whom to retrieve the released DAI amount. + + @return amountReleased The total amount of DAI (in WEI) released to the payee. + */ function released(address account) public view override returns (uint256 amountReleased) { amountReleased = _released[account]; return amountReleased; } + /** + @notice This function verifies if a specified address is registered as a payee in the contract. + + @dev This function is a view function and does not modify the contract's state. It iterates through the + internal `_payees` array to check if the provided `account` address exists within the list of registered payees. + + @param account The address to be checked against the registered payees. + + @return accountIsPayee True if the address is a registered payee, False otherwise. + */ function isPayee(address account) public view override returns (bool accountIsPayee) { uint256 nrOfPayees = _payees.length; accountIsPayee = false; @@ -152,10 +261,26 @@ contract CustomPaymentSplitter is Interface { } /** - * @dev Add a new payee to the contract. - * @param account The address of the payee to add. - * @param dai_ The number of dai owned by the payee. - */ + @notice This private function adds a new payee to the contract. + + @dev This function is private and can only be called by other functions within the contract. It performs the + following actions: + + 1. Validates that the payee address is not already registered (by checking if the corresponding `wei` share balance + is zero). + + 2. Adds the payee's address to the internal `_payees` array. + + 3. Sets the payee's initial share balance in the `_dai` mapping. + + 4. Updates the contract-wide total DAI amount to reflect the addition of the new payee's share. + + 5. Emits a `PayeeAdded` event to log the details of the new payee. + + @param account The address of the payee to be added. + @param dai_ The amount of wei allocated as the payee's initial share. + + */ function _addPayee(address account, uint256 dai_) private { require(_dai[account] == 0, "This account already is owed some currency."); diff --git a/src/DecentralisedInvestmentManager.sol b/src/DecentralisedInvestmentManager.sol index 3499cf0..698cbdf 100644 --- a/src/DecentralisedInvestmentManager.sol +++ b/src/DecentralisedInvestmentManager.sol @@ -5,7 +5,7 @@ import { Tier } from "../src/Tier.sol"; import { TierInvestment } from "../src/TierInvestment.sol"; import { SaasPaymentProcessor } from "../src/SaasPaymentProcessor.sol"; -import { DecentralisedInvestmentHelper } from "../src/Helper.sol"; +import { Helper } from "../src/Helper.sol"; import { CustomPaymentSplitter } from "../src/CustomPaymentSplitter.sol"; import { WorkerGetReward } from "../src/WorkerGetReward.sol"; import { ReceiveCounterOffer } from "../src/ReceiveCounterOffer.sol"; @@ -58,7 +58,7 @@ contract DecentralisedInvestmentManager is Interface { ReceiveCounterOffer private _receiveCounterOffer; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; SaasPaymentProcessor private _saasPaymentProcessor; TierInvestment[] private _tierInvestments; @@ -85,10 +85,19 @@ contract DecentralisedInvestmentManager is Interface { } /** - * Constructor for creating a Tier instance. The values cannot be changed - * after creation. - * - */ + @notice This contract manages a decentralized investment process. + + @dev This contract facilitates fundraising for a project by allowing investors to contribute + currency based on predefined tiers. It tracks the total amount raised, distributes rewards, + and handles withdrawals for the project lead and potentially other parties. + + @param tiers: An array of `Tier` objects defining the different investment tiers. + @param projectLeadFracNumerator: Numerator representing the project lead's revenue share. + @param projectLeadFracDenominator: Denominator representing the project lead's revenue share. + @param projectLead: The address of the project lead. + @param raisePeriod: The duration of the fundraising campaign in seconds (uint32). + @param investmentTarget: The total amount of DAI the campaign aims to raise. + */ constructor( Tier[] memory tiers, uint256 projectLeadFracNumerator, @@ -103,7 +112,7 @@ contract DecentralisedInvestmentManager is Interface { _projectLead = projectLead; // Initialise contract helper. - _helper = new DecentralisedInvestmentHelper(); + _helper = new Helper(); _saasPaymentProcessor = new SaasPaymentProcessor(); _startTime = block.timestamp; @@ -225,7 +234,11 @@ contract DecentralisedInvestmentManager is Interface { } /** - @notice when an investor makes an investment with its investmentWallet, this + @notice This function is called to process a SAAS payment received by the contract. + It splits the revenue between the project lead and investors based on a predefined + ratio. + + When an investor makes an investment with its investmentWallet, this contract checks whether the contract is full, or whether it still takes in new investments. If the investment ceiling is reached it reverts the investment back to the investor. Otherwise it takes it in, and fills up the @@ -240,7 +253,15 @@ contract DecentralisedInvestmentManager is Interface { recursively calls itself until the whole investment is distributed, or the investment ceiling is reached. In case of the latter, the remaining investment amount is returned. - */ + + @dev This function first validates that the received SAAS payment is greater than + zero. Then, it calculates the revenue distribution for the project lead and investors + using helper functions. It performs a sanity check to ensure the distribution matches + the total received amount. Finally, it distributes the investor's portion + (if any) based on their investment tier and remaining return. + + + */ function receiveInvestment() external payable override { require(msg.value > 0, "The amount invested was not larger than 0."); @@ -254,6 +275,22 @@ contract DecentralisedInvestmentManager is Interface { emit InvestmentReceived(msg.sender, msg.value); } + /** + @notice This function allows an investor to finalize an investment based on a + previously accepted counter-offer. This can only be called by the + _receiveCounterOffer contract. + + @dev This function validates that the investment amount is greater than zero + and the caller is authorized (the `ReceiveCounterOffer` contract). It also checks + if the investment ceiling has not been reached. If all requirements are met, + the function allocates the investment and emits an `InvestmentReceived` event. + TODO: specify and test exactly what happens if a single investment overshoots + the investment ceiling. + + @param offerInvestor The address of the investor finalizing the investment. + + + */ function receiveAcceptedOffer(address payable offerInvestor) public payable override { require(msg.value > 0, "The amount invested was not larger than 0."); require( @@ -266,6 +303,21 @@ contract DecentralisedInvestmentManager is Interface { emit InvestmentReceived(offerInvestor, msg.value); } + /** + @notice This function allows the project lead to instantly increase the current + investment tier multiplier. (An ROI decrease is not possible.) It does not + increase the ROI multiple of previous investments, it only increases the ROI + multiple of any new investments. + + @dev This function restricts access to the project lead only. It validates that + the new multiplier is strictly greater than the current tier's multiplier. + If the requirements are met, it directly updates the current tier's multiplier + with the new value. + + @param newMultiple The new multiplier to be applied to the current investment tier. + + + */ function increaseCurrentMultipleInstantly(uint256 newMultiple) public override { require( msg.sender == _projectLead, @@ -276,7 +328,18 @@ contract DecentralisedInvestmentManager is Interface { currentTier.increaseMultiple(newMultiple); } - // Allow project lead to retrieve the investment. + /** + @notice This function allows the project lead to withdraw funds from the investment pool. + + @dev This function restricts access to the project lead only. It verifies that + the contract has sufficient balance and the investment target has been reached + before allowing a withdrawal. It then transfers the requested amount to the + project lead's address using a secure `call{value: }` approach. + + @param amount The amount of DAI the project lead wants to withdraw. + + + */ function withdraw(uint256 amount) public override { // Ensure only the project lead can retrieve funds in this contract. The // funds in this contract are those coming from investments. Saaspayments are @@ -287,52 +350,152 @@ contract DecentralisedInvestmentManager is Interface { require(address(this).balance >= amount, "Insufficient contract balance"); require(_cumReceivedInvestments >= _investmentTarget, "Investment target is not yet reached."); - // Transfer funds to user using call{value: } (safer approach) + // Transfer funds to user using call{value: } (safer approach). (bool success, ) = payable(msg.sender).call{ value: amount }(""); require(success, "Investment withdraw by project lead failed"); } - // Assuming there's an internal function to get tier investment length + /** + @notice This function retrieves the total number of investment tiers currently + registered in the contract. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It fetches the length of the internal `_tierInvestments` array + which stores information about each investment tier. + + @return nrOfTierInvestments The total number of registered investment tiers. + */ function getTierInvestmentLength() public view override returns (uint256 nrOfTierInvestments) { nrOfTierInvestments = _tierInvestments.length; return nrOfTierInvestments; } + /** + @notice This function retrieves the address of the `CustomPaymentSplitter` contract + used for managing project lead and investor withdrawals. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It returns the address stored in the internal `_paymentSplitter` + variable. + + @return paymentSplitter The address of the `CustomPaymentSplitter` contract. + */ function getPaymentSplitter() public view override returns (CustomPaymentSplitter paymentSplitter) { paymentSplitter = _paymentSplitter; return paymentSplitter; } + /** + @notice This function retrieves the total amount of wei currently raised by the + investment campaign. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It returns the value stored in the internal `_cumReceivedInvestments` + variable which keeps track of the total amount of investments received. + + @return cumReceivedInvestments The total amount of wei collected through investments. + */ function getCumReceivedInvestments() public view override returns (uint256 cumReceivedInvestments) { cumReceivedInvestments = _cumReceivedInvestments; return cumReceivedInvestments; } + /** + @notice This function retrieves the total remaining return amount available for + investors based on the current investment pool and defined tiers. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It utilizes the helper contract `_helper` to calculate the cumulative + remaining investor return based on the current investment tiers and the total + amount of wei raised. + + @return cumRemainingInvestorReturn The total remaining amount of wei available for investor returns. + */ function getCumRemainingInvestorReturn() public view override returns (uint256 cumRemainingInvestorReturn) { return _helper.computeCumRemainingInvestorReturn(_tierInvestments); } + /** + @notice This function retrieves the investment tier that corresponds to the current + total amount of wei raised. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It utilizes the helper contract `_helper` to determine the current tier + based on the predefined investment tiers and the total amount collected through + investments (`_cumReceivedInvestments`). + + @return currentTier An object representing the current investment tier. + */ function getCurrentTier() public view override returns (Tier currentTier) { currentTier = _helper.computeCurrentInvestmentTier(_cumReceivedInvestments, _tiers); return currentTier; } + /** + @notice This function retrieves the fraction of the total revenue allocated to + the project lead. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It returns the value stored in the internal + `_projectLeadFracNumerator` variable, which represents the numerator of the + fraction defining the project lead's revenue share (expressed in WEI). + + @return projectLeadFracNumerator The numerator representing the project lead's revenue share fraction (WEI). + */ function getProjectLeadFracNumerator() public view override returns (uint256 projectLeadFracNumerator) { projectLeadFracNumerator = _projectLeadFracNumerator; return projectLeadFracNumerator; } + /** + @notice This function retrieves the `ReceiveCounterOffer` contract + used for processing counter-offers made to investors. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It returns the address stored in the internal `_receiveCounterOffer` + variable. + + @return `ReceiveCounterOffer` contract. + */ function getReceiveCounterOffer() public view override returns (ReceiveCounterOffer) { return _receiveCounterOffer; } + /** + @notice This function retrieves the `WorkerGetReward` contract + used for managing project worker reward distribution. + + @dev This function is a view function, meaning it doesn't modify the contract's + state. It returns the address stored in the internal `_workerGetReward` + variable. + + @return address The address of the `WorkerGetReward` contract. + */ function getWorkerGetReward() public view override returns (WorkerGetReward) { return _workerGetReward; } - // Function that can be called externally to trigger returnAll if conditions are met + /** + @notice This function allows the project lead to initiate a full investor return + in case the fundraising target is not met by the deadline. + + @dev This function can only be called by the project lead after the fundraising + delay has passed and the investment target has not been reached. It iterates + through all investment tiers and transfers the invested amounts back to the + corresponding investors using a secure `transfer` approach. Finally, it verifies + that the contract balance is zero after the return process. + + **Important Notes:** + + * This function is designed as a safety measure and should only be called if + the project fails to reach its funding target. + * Project owners should carefully consider the implications of returning funds + before calling this function. + + + */ function triggerReturnAll() public onlyAfterDelayAndUnderTarget { - // TODO: return all investments. + require(msg.sender == _projectLead, "Someone other than projectLead tried to return all investments."); uint256 nrOfTierInvestments = _tierInvestments.length; for (uint256 i = 0; i < nrOfTierInvestments; ++i) { // Transfer the amount to the PaymentSplitter contract @@ -342,17 +505,32 @@ contract DecentralisedInvestmentManager is Interface { } /** - @notice If the investment ceiling is not reached, it finds the lowest open - investment tier, and then computes how much can still be invested in that - investment tier. If the investment amount is larger than the amount remaining - in that tier, it fills that tier up with a part of the investment using the - addInvestmentToCurrentTier function, and recursively calls itself until the - investment amount is fully allocated, or Investment ceiling is reached. - If the investment amount is equal to- or smaller than the amount remaining in - that tier, it adds that amount to the current investment tier using the - addInvestmentToCurrentTier. That's it. - - Made internal instead of private for testing purposes. + @notice This internal function allocates a received investment to the appropriate + tierInvestment contract(s). + + @dev This function first validates that the investment amount is greater than + zero. It then checks if the investment ceiling has been reached. If not, it + determines the current investment tier and the remaining amount available in that + tier. It allocates the investment following these steps: + + 1. If the investment amount is greater than the remaining amount in the current + tier: + - Invest the remaining amount in the current tier. + - Recursively call `_allocateInvestment` with the remaining investment amount + to allocate the remaining funds to subsequent tiers. + + 2. If the investment amount is less than or equal to the remaining amount in the + current tier: + - Invest the full amount in the current tier. + + The function utilizes the helper contract `_saasPaymentProcessor` to perform the + tier-based investment allocation and keeps track of all investments using the + `_tierInvestments` array. + + @param investmentAmount The amount of WEI invested by the investor. + @param investorWallet The address of the investor's wallet. + + */ function _allocateInvestment(uint256 investmentAmount, address investorWallet) internal { require(investmentAmount > 0, "The amount invested was not larger than 0."); @@ -402,6 +580,35 @@ contract DecentralisedInvestmentManager is Interface { } /** + @notice This internal function allocates SAAS revenue to a designated wallet address. + + @dev This function performs the following actions: + + 1. Validates that the contract has sufficient balance to cover the allocation amount + and that the allocation amount is greater than zero. + + 2. Transfers the allocation amount (in WEI) to the `CustomPaymentSplitter` contract + using a secure `call{value: }` approach. The call includes the `deposit` function + signature and the receiving wallet address as arguments. + + 3. Verifies the success of the transfer. + + 4. Checks if the receiving wallet is already registered as a payee in the + `CustomPaymentSplitter` contract. + + - If not, it calls the `publicAddPayee` function of the `CustomPaymentSplitter` + contract to add the receiving wallet as a payee with the allocated amount as + its initial share. + + - If the receiving wallet is already a payee, it calls the + `publicAddSharesToPayee` function of the `CustomPaymentSplitter` contract to + increase the existing payee's share by the allocated amount. + + **Important Notes:** + + * This function assumes the existence of a `CustomPaymentSplitter` contract + deployed and properly configured for managing project revenue distribution. + This contract does not check who calls it, which sounds risky, but it is an internal contract, which means it can only be called by this contract or contracts that derive from this one. I assume contracts that derive from this contract are contracts that are initialised within this @@ -410,13 +617,15 @@ contract DecentralisedInvestmentManager is Interface { In essence, other functions should not allow calling this function with an amount or wallet address that did not correspond to a SAAS payment. Since this function is only called by receiveSaasPayment function (w.r.t. non-test functions), which contains the - logic to only call this if a avlid SAAS payment is received, this is safe. + logic to only call this if a avlid SAAS payment is received, this is safe. - Ideally one would make it private instead of internal, such that only this contract is - able to call this function, however, to also allow tests to reach this contract, it is - made internal. - TODO: include safe handling of gas costs. - */ + Ideally one would make it private instead of internal, such that only this contract is + able to call this function, however, to also allow tests to reach this contract, it is + made internal. + + @param amount The amount of WEI to be allocated as SAAS revenue. + @param receivingWallet The address of the wallet that should receive the allocation. + */ function _performSaasRevenueAllocation(uint256 amount, address receivingWallet) internal { require(address(this).balance >= amount, "Error: Insufficient contract balance."); require(amount > 0, "The SAAS revenue allocation amount was not larger than 0."); diff --git a/src/Helper.sol b/src/Helper.sol index ca13745..83866bb 100644 --- a/src/Helper.sol +++ b/src/Helper.sol @@ -36,10 +36,21 @@ interface Interface { uint256 investorFracNumerator, uint256 investorFracDenominator, uint256 paidAmount - ) external pure returns (uint256 returneCumRemainingInvestorReturn); + ) external pure returns (uint256 returnCumRemainingInvestorReturn); } -contract DecentralisedInvestmentHelper is Interface { +contract Helper is Interface { + /** + @notice This function calculates the total remaining investment return across all TierInvestments. + + @dev This function is a view function and does not modify the contract's state. It iterates through a provided array + of `TierInvestment` objects and accumulates the `getRemainingReturn` values from each TierInvestment. + + @param tierInvestments An array of `TierInvestment` objects representing an investment in a specific investment Tier. + + @return cumRemainingInvestorReturn The total amount of WEI remaining to be returned to all investors across all + tiers. + */ function computeCumRemainingInvestorReturn( TierInvestment[] memory tierInvestments ) public view override returns (uint256 cumRemainingInvestorReturn) { @@ -57,6 +68,17 @@ contract DecentralisedInvestmentHelper is Interface { return cumRemainingInvestorReturn; } + /** + @notice This function retrieves the investment ceiling amount from the provided investment tiers. + + @dev This function is a view function and does not modify the contract's state. It assumes that the investment tiers + are ordered with the highest tier at the end of the provided `tiers` array. The function retrieves the `getMaxVal` + from the last tier in the array, which represents the maximum investment allowed according to the configured tiers. + + @param tiers An array of `Tier` structs representing the investment tiers and their configurations. + + @return investmentCeiling The investment ceiling amount (in WEI) defined by the highest tier. + */ function getInvestmentCeiling(Tier[] memory tiers) public view override returns (uint256 investmentCeiling) { // Access the last tier in the array @@ -67,6 +89,19 @@ contract DecentralisedInvestmentHelper is Interface { return investmentCeiling; } + /** + @notice This function determines if the total amount of received investments has reached the investment ceiling. + + @dev This function is a view function and does not modify the contract's state. It compares the provided + `cumReceivedInvestments` (total received WEI) to the investment ceiling retrieved by calling + `getInvestmentCeiling(tiers)`. + + @param cumReceivedInvestments The cumulative amount of WEI received from investors. + @param tiers An array of `Tier` structs representing the investment tiers and their configurations. + + @return reachedInvestmentCeiling True if the total received investments have reached or exceeded the investment + ceiling, False otherwise. + */ function hasReachedInvestmentCeiling( uint256 cumReceivedInvestments, Tier[] memory tiers @@ -75,6 +110,25 @@ contract DecentralisedInvestmentHelper is Interface { return reachedInvestmentCeiling; } + /** + @notice This function identifies the current investment tier based on the total received investments. + + @dev This function is a view function and does not modify the contract's state. It iterates through the provided + `tiers` array to find the tier where the `cumReceivedInvestments` (total received WEI) falls within the defined + investment range. + + @param cumReceivedInvestments The cumulative amount of WEI received from investors. + @param tiers An array of `Tier` structs representing the investment tiers and their configurations. + + @return currentTier The `Tier` struct representing the current investment tier based on the received investments. + + **Important Notes:** + + * The function assumes that the `tiers` array is properly configured with increasing investment thresholds. The tier + with the highest threshold should be positioned at the end of the array. + * If the `cumReceivedInvestments` reach or exceed the investment ceiling defined by the tiers, the function reverts + with a `ReachedInvestmentCeiling` error. + */ function computeCurrentInvestmentTier( uint256 cumReceivedInvestments, Tier[] memory tiers @@ -107,6 +161,20 @@ contract DecentralisedInvestmentHelper is Interface { ); } + /** + @notice This function determines whether to check the next investment tier based on the current iteration index and + the cumulative received investments. + @dev This function is a view function and does not modify the contract's state. It checks if the cumulative received + investments fall within the range of the current investment tier. It exists because Solidity does not do lazy + evaluation, meaning the tiers[i] may yield an out of range error, if the i>= nrOfTiers check is not performed in + advance. However the entire function needs to be checked in a single while loop conditional, hence the separate + function. + @param i The current iteration index. + @param nrOfTiers The total number of investment tiers. + @param tiers An array of `Tier` structs representing the investment tiers and their configurations. + @param cumReceivedInvestments The total amount of wei received from investors. + @return bool True if the next investment tier should be checked, false otherwise. + */ function shouldCheckNextStage( uint256 i, uint256 nrOfTiers, @@ -120,6 +188,19 @@ contract DecentralisedInvestmentHelper is Interface { } } + /** + @notice This function calculates the remaining amount of investment that is enough to fill the current investment + tier. + + @dev This function is designed to be used within investment contracts to track progress towards different investment + tiers. It assumes that the Tier struct has properties getMinVal and getMaxVal which define the minimum and maximum + investment amounts for the tier, respectively. + + @param cumReceivedInvestments The total amount of wei received in investments so far. + @param someTier The investment tier for which to calculate the remaining amount. + + @return remainingAmountInTier The amount of wei remaining to be invested to reach the specified tier. + **/ function getRemainingAmountInCurrentTier( uint256 cumReceivedInvestments, Tier someTier @@ -141,44 +222,92 @@ contract DecentralisedInvestmentHelper is Interface { return remainingAmountInTier; } + /** + @notice This function calculates the remaining amount of wei available for payout to investors after the current + payout is distributed. + + @dev This function considers the following factors: + + The total remaining amount available for investor payout (cumRemainingInvestorReturn). + The investor's fractional share of the pool (investorFracNumerator / investorFracDenominator). + The amount of wei currently being paid out (paidAmount). + + The function employs a tiered approach to determine the payout amount for investors: + + If there are no remaining funds for investors (cumRemainingInvestorReturn == 0), the function returns 0. + If the investor's owed amount is less than the current payout + (cumRemainingInvestorReturn * investorFracDenominator < paidAmount * (investorFracNumerator)), the function pays + out the investor's entire remaining balance (cumRemainingInvestorReturn). + Otherwise, the function calculates the investor's payout based on their fractional share of the current payment + (paidAmount). In this case, a division with rounding-up is performed to ensure investors receive their full + entitlement during their final payout. + + @param cumRemainingInvestorReturn The total amount of wei remaining for investor payout before the current + distribution. + @param investorFracNumerator The numerator representing the investor's fractional share. + @param investorFracDenominator The denominator representing the investor's fractional share. + @param paidAmount The amount of wei being distributed in the current payout. + + @return returnCumRemainingInvestorReturn The remaining amount of wei available for investor payout after the current + distribution. + + **/ function computeRemainingInvestorPayout( uint256 cumRemainingInvestorReturn, uint256 investorFracNumerator, uint256 investorFracDenominator, uint256 paidAmount - ) public pure override returns (uint256 returneCumRemainingInvestorReturn) { + ) public pure override returns (uint256 returnCumRemainingInvestorReturn) { require( investorFracDenominator >= investorFracNumerator, "investorFracNumerator is smaller than investorFracDenominator." ); + // If the investors are made whole, return 0. if (cumRemainingInvestorReturn == 0) { - returneCumRemainingInvestorReturn = 0; - return returneCumRemainingInvestorReturn; + returnCumRemainingInvestorReturn = 0; + return returnCumRemainingInvestorReturn; - // Check if the amount to be paid to the investor is smaller than the - // amount the investors can receive based on the investorFraction and the - // incoming SAAS payment amount. If so, just pay out what the investors - // can receive in whole. + // Check if the investor is owed less than the amount of SAAS revenue available. If so, just pay the investor in + // whole. } else if (cumRemainingInvestorReturn * investorFracDenominator < paidAmount * (investorFracNumerator)) { - // In this case, the investors fraction of the SAAS payment is more than - // what they still can get, so just return what they can still receive. - returneCumRemainingInvestorReturn = cumRemainingInvestorReturn; - return returneCumRemainingInvestorReturn; - } else { - // In this case, there is not enough SAAS payment received to make the - // investors whole with this single payment, so instead they get their - // fraction of the SAAS payment. + returnCumRemainingInvestorReturn = cumRemainingInvestorReturn; + return returnCumRemainingInvestorReturn; - // Perform division with roundup to ensure the invstors are paid in whole - // during their last payout without requiring an additional 1 wei payout. + // In this case, there is not enough SAAS payment received to make the investors whole with this single payment, + // so instead they get their fraction of the SAAS payment. + } else { + // Perform division with roundup to ensure the invstors are paid in whole during their last payout without + // requiring an additional 1 wei payout. uint256 numerator = paidAmount * investorFracNumerator; uint256 denominator = investorFracDenominator; - returneCumRemainingInvestorReturn = numerator / denominator + (numerator % denominator == 0 ? 0 : 1); - return returneCumRemainingInvestorReturn; + returnCumRemainingInvestorReturn = numerator / denominator + (numerator % denominator == 0 ? 0 : 1); + return returnCumRemainingInvestorReturn; } } + /** + @notice This function determines whether a division yields a whole number or not. + + @dev This function is specifically designed for scenarios where division with rounding-up is required once to ensure + investors receive their full entitlement during their final payout. It takes two arguments: + + withRounding: The dividend in the division operation with rounding-up. + roundDown: The result of dividing the dividend by the divisor without rounding. + + The function performs a simple comparison between the dividend and the result of the division without rounding. If + they are equal, it implies no remainder exists after rounding-up, and the function returns false. Otherwise, a + remainder exists, and the function returns true. + + @param withRounding The dividend to be used in the division operation with rounding-up. + @param roundDown The result of dividing the dividend by the divisor without rounding. + + @return boolIsWholeDivision A boolean indicating whether the division with rounding-up would result in a remainder: + + true: There would be a remainder after rounding-up the division. + false: There would be no remainder after rounding-up the division. + + **/ function isWholeDivision( uint256 withRounding, uint256 roundDown @@ -187,6 +316,26 @@ contract DecentralisedInvestmentHelper is Interface { return boolIsWholeDivision; } + /** + @notice This function checks whether a given value lies within a specific inclusive range. + + @dev This function is useful for validating inputs or performing operations within certain value boundaries. It takes + three arguments: + + minVal: The minimum value of the inclusive range. + maxVal: The maximum value of the inclusive range. + someVal: The value to be checked against the range. + + @param minVal The minimum value of the inclusive range. + @param maxVal The maximum value of the inclusive range. + @param someVal The value to be checked against the range. + + @return inRange A boolean indicating whether the value is within the specified range: + + true: The value is within the inclusive range (minVal <= someVal < maxVal). + false: The value is outside the inclusive range. + + **/ function isInRange(uint256 minVal, uint256 maxVal, uint256 someVal) public pure override returns (bool inRange) { if (minVal <= someVal && someVal < maxVal) { inRange = true; diff --git a/src/ReceiveCounterOffer.sol b/src/ReceiveCounterOffer.sol index 27bb2f2..e95da63 100644 --- a/src/ReceiveCounterOffer.sol +++ b/src/ReceiveCounterOffer.sol @@ -33,19 +33,86 @@ contract ReceiveCounterOffer is Interface { address private _owner; /** - * Constructor for creating a Tier instance. The values cannot be changed - * after creation. - * - */ + @notice This contract serves as a framework for facilitating counteroffers within a project. + + @dev This contract facilitates the exchange of counteroffers between an investor and the project lead. + + Key features: + + Tracks counteroffer details, including offer multiplier and duration. + Maintains the offer start time for tracking validity. + Stores booleans to indicate offer acceptance and decision status. + Holds an array of Offer structs to manage historical offers. + Tracks both project lead and owner addresses. + + @param projectLead The address of the project lead who can make and accept counteroffers. + **/ constructor(address projectLead) public { _owner = payable(msg.sender); _projectLead = projectLead; } + /** + @notice This function allows the investor to make an offer with an investment amount and ROI multiple different than + what is proposed in the current investment tier. + + @dev This function creates a new Offer struct and adds it to the internal _offers array. The Offer struct captures + details about the counteroffer, including: + + The address of the investor making the offer. + The amount of wei offered by the recipient (stored in amount). + The ROI multiplier proposed for the initial investment value (stored in multiplier). + The duration in seconds for the project lead to consider the offer (stored in duration) + The timestamp of the offer creation (stored in timestamp). + A boolean flag indicating offer acceptance (initially set to false in accepted) + A boolean flag indicating whether the project lead has made a decision (initially set to false in decided). + + Requirements: + The investor must also attach a payment along with the offer (enforced by the payable modifier). (The investor + can get its money back if the offer is rejected or expired.) + + @param multiplier The multiplier to be applied to the initial project value. + @param duration The duration in seconds for the project lead to consider the counteroffer. + + **/ function makeOffer(uint16 multiplier, uint256 duration) external payable override { _offers.push(Offer(payable(msg.sender), msg.value, multiplier, duration, block.timestamp, false, false)); } + /** + @notice This function allows the project lead to accept or reject an offer made by an investor. + + @dev This function enables the project lead to make a decision on an offer stored in the internal _offers array. The + function performs the following actions: + + Validates that the function caller is the project lead (enforced by the require statement + with msg.sender == _projectLead). + + Ensures the offer hasn't already been decided upon (enforced by the require statement + with !_offers[offerId]._isDecided). + + Checks if the offer is still valid by comparing the current timestamp with the offer's start time and duration. + + If the project lead accepts the offer (indicated by accept being true): + The offer's _offerIsAccepted flag is set to true. + The offer's _isDecided flag is set to true, signifying a decision has been made. + The DecentralisedInvestmentManager contract is called using the receiveAcceptedOffer function. This function + likely handles tasks related to finalizing the investment based on the accepted offer details (investor + address, investment amount). The value parameter of the function call ensures the appropriate investment + amount is transferred. + + If the project lead rejects the offer (indicated by accept being false): + The offer's _offerIsAccepted flag is set to false. + The offer's _isDecided flag is set to true, signifying a decision has been made. + + Limitations: + + This function assumes the existence of a DecentralisedInvestmentManager contract and its receiveAcceptedOffer + function. The specific implementation details of that function are not defined here. + + @param offerId The unique identifier of the offer within the _offers array. + @param accept A boolean indicating the project lead's decision (true for accept, false for reject). + **/ function acceptOrRejectOffer(uint256 offerId, bool accept) public override { require(msg.sender == _projectLead, "Only project lead can accept offer"); @@ -67,6 +134,27 @@ contract ReceiveCounterOffer is Interface { } } + /** + @notice This function allows an investor to withdraw their investment offer. + + @dev This function enables the investor to retract an offer stored in the _offers array, but only under certain + conditions: + + The function caller must be the investor who made the offer (verified by comparing msg.sender with the + _offerInvestor address stored in the offer). + If the project lead has already made a decision (indicated by _isDecided being true): + The investor can only withdraw the offer if it was not accepted (enforced by the require statement with + !_offers[offerId]._offerIsAccepted). + If the project lead hasn't made a decision yet (indicated by _isDecided being false): + The investor can only withdraw the offer if the offer duration has expired (enforced by the require statement + with block.timestamp > _offers[offerId]._offerStartTime + _offers[offerId]._offerDuration). + + If the withdrawal conditions are met, the function transfers the investor's original investment amount back to the + investor's address using the transfer function. + + @param offerId The unique identifier of the offer within the _offers array. + + */ function pullbackOffer(uint256 offerId) public override { require(msg.sender == _offers[offerId]._offerInvestor, "Someone other than the investor tried to retrieve offer."); if (_offers[offerId]._isDecided) { diff --git a/src/SaasPaymentProcessor.sol b/src/SaasPaymentProcessor.sol index 1fd3760..cc6cc3d 100644 --- a/src/SaasPaymentProcessor.sol +++ b/src/SaasPaymentProcessor.sol @@ -2,20 +2,20 @@ pragma solidity >=0.8.23; // Specifies the Solidity compiler version. import { Tier } from "../src/Tier.sol"; import { TierInvestment } from "../src/TierInvestment.sol"; -import { DecentralisedInvestmentHelper } from "../src/Helper.sol"; +import { Helper } from "../src/Helper.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "forge-std/src/console2.sol"; // Import the console library interface Interface { function computeInvestorReturns( - DecentralisedInvestmentHelper helper, + Helper helper, TierInvestment[] memory tierInvestments, uint256 saasRevenueForInvestors, uint256 cumRemainingInvestorReturn ) external returns (TierInvestment[] memory, uint256[] memory); function computeInvestmentReturn( - DecentralisedInvestmentHelper helper, + Helper helper, uint256 remainingReturn, uint256 saasRevenueForInvestors, uint256 cumRemainingInvestorReturn, @@ -41,12 +41,32 @@ contract SaasPaymentProcessor is Interface { _; } + /** + @notice Initializes the SaasPaymentProcessor contract by setting the contract creator as the owner. + @dev This constructor sets the sender of the transaction as the owner of the contract. + */ constructor() public { _owner = msg.sender; } + /** + @notice Computes the payout for investors based on their tier investments and the SAAS revenue and stores it as the + remaining return in the TierInvestment objects. + @dev This function calculates the returns for investors in each tierInvestment based on their investments and the + total SAAS revenue. It ensures that the cumulative payouts match the SAAS revenue. The ROIs are then stored in the + tierInvestment objects as remaining return. + + @param helper An instance of the Helper contract. + @param tierInvestments An array of `TierInvestment` structs representing the investments made by investors in each + tier. + @param saasRevenueForInvestors The total SAAS revenue allocated for investor returns. + @param cumRemainingInvestorReturn The cumulative remaining return amount for investors. + + @return _returnTiers An array of `TierInvestment` structs representing the tiers for which returns are computed. + @return _returnAmounts An array of uint256 values representing the computed returns for each tier. + */ function computeInvestorReturns( - DecentralisedInvestmentHelper helper, + Helper helper, TierInvestment[] memory tierInvestments, uint256 saasRevenueForInvestors, uint256 cumRemainingInvestorReturn @@ -62,7 +82,6 @@ contract SaasPaymentProcessor is Interface { for (uint256 i = 0; i < nrOfTierInvestments; ++i) { // Compute how much an investor receives for its investment in this tier. - (uint256 investmentReturn, bool returnHasRoundedUp) = computeInvestmentReturn( helper, tierInvestments[i].getRemainingReturn(), @@ -99,12 +118,19 @@ contract SaasPaymentProcessor is Interface { } /** - @notice This creates a tierInvestment object/contract for the current tier. - Since it takes in the current tier, it stores the multiple used for that tier - to specify how much the investor may retrieve. Furthermore, it tracks how - much investment this contract has received in total using - _cumReceivedInvestments. - */ + @notice Creates TierInvestment & updates total investment. + + @dev Creates a new TierInvestment for an investor in the current tier. Then increments total investment received. + Since it takes in the current tier, it stores the multiple used for that current tier. + Furthermore, it tracks how much investment this contract has received in total using _cumReceivedInvestments. + + @param cumReceivedInvestments Total investment received before this call. + @param investorWallet Address of the investor. + @param currentTier The tier the investment belongs to. + @param newInvestmentAmount The amount of wei invested. + + @return A tuple of (updated total investment, new TierInvestment object). + **/ function addInvestmentToCurrentTier( uint256 cumReceivedInvestments, address investorWallet, @@ -117,7 +143,16 @@ contract SaasPaymentProcessor is Interface { } /** - @dev Since this is an integer division, which is used to allocate shares, + @dev + */ + /** + @notice Calculates investment return for investors based on remaining return and investor share. + + @dev This function computes the investment return for investors based on the remaining return available for + distribution and the total cumulative remaining investor return. It employs integer division, which discards + decimals. + + Since this is an integer division, which is used to allocate shares, the decimals that are discarded by the integer division, in total would add up to 1, if the shares are not exact division. Therefore, this function compares the results of the division, with round down vs round up. If the two @@ -134,9 +169,20 @@ contract SaasPaymentProcessor is Interface { fraction gets +1 wei to ensure all the numbers add up correctly. A difference of +- wei is considederd negligible w.r.t. to the investor return, yet critical in the safe evaluation of this contract. - */ + + + @param helper (Helper): A reference to a helper contract likely containing the isWholeDivision function. + @param remainingReturn (uint256): The total remaining wei to be distributed to investors. + @param saasRevenueForInvestors (uint256): The total SaaS revenue allocated to investors. + @param cumRemainingInvestorReturn (uint256): The total cumulative remaining investor return used as the divisor for + calculating share ratios. + @param incomingHasRoundedUp (bool): A boolean flag indicating if a previous calculation rounded up. + + @return investmentReturn The calculated investment return for the current investor (uint256). + @return returnedHasRoundedUp A boolean indicating if this function rounded up the share (bool). + **/ function computeInvestmentReturn( - DecentralisedInvestmentHelper helper, + Helper helper, uint256 remainingReturn, uint256 saasRevenueForInvestors, uint256 cumRemainingInvestorReturn, diff --git a/src/Tier.sol b/src/Tier.sol index 742c7ba..1b52d0d 100644 --- a/src/Tier.sol +++ b/src/Tier.sol @@ -19,10 +19,15 @@ contract Tier is ITier { address private _owner; /** - * Constructor for creating a Tier instance. The values cannot be changed - * after creation. - * - */ + @notice Constructor for creating a Tier instance with specified configuration parameters. + @dev This constructor initializes a Tier instance with the provided minimum value, maximum value, and ROI multiple. + The values cannot be changed after creation. + + Consecutive Tier objects are expected to have touching maxVal and minVal values respectively. + @param minVal The minimum investment amount for this tier. + @param maxVal The maximum investment amount for this tier. + @param multiple The ROI multiple for this tier. + */ constructor(uint256 minVal, uint256 maxVal, uint256 multiple) public { _owner = msg.sender; // Improved error message using string concatenation @@ -47,22 +52,51 @@ contract Tier is ITier { _multiple = multiple; } + /** + @notice Increases the ROI multiple for this Tier object. + @dev This function allows the project lead to increase the ROI multiple for this Tier object. It requires that the + caller is the owner of the Tier object and that the new integer multiple is larger than the current integer multiple. + @param newMultiple The new ROI multiple to set for this Tier object. + */ function increaseMultiple(uint256 newMultiple) public virtual override { require(msg.sender == _owner, "Increasing the Tier object multiple attempted by someone other than project lead."); require(newMultiple > _multiple, "The new multiple was not larger than the old multiple."); _multiple = newMultiple; } + /** + @notice This function retrieves the investment starting amount at which this Tier begins. + + @dev This value can be used to determine if the current investment level is in this tier or not. + + @return minVal The minimum allowed value. + */ function getMinVal() public view override returns (uint256 minVal) { minVal = _minVal; return minVal; } + /** + @notice This function retrieves the investment ceiling amount at which this Tier ends. + + @dev This value can be used to determine if the current investment level is in this tier or not. + + @return maxVal The minimum allowed value. + */ function getMaxVal() public view override returns (uint256 maxVal) { maxVal = _maxVal; return maxVal; } + /** + @notice This function retrieves the current ROI multiple that is used for all investments that are allocated in this + Tier. + + @dev This value is used to compute how much an investor may receive as return on investment. An investment of + 5 ether at a multiple of 6 yields can yield a maximum profit of 25 ether, if sufficient SAAS revenue comes in. + + @return multiple The current multiplication factor. + */ function getMultiple() public view override returns (uint256 multiple) { multiple = _multiple; return multiple; diff --git a/src/TierInvestment.sol b/src/TierInvestment.sol index b2dbff5..2b19ef3 100644 --- a/src/TierInvestment.sol +++ b/src/TierInvestment.sol @@ -41,10 +41,14 @@ contract TierInvestment is Interface { } /** - * Constructor for creating a Tier instance. The values cannot be changed - * after creation. - * - */ + @notice This function is the constructor used to create a new TierInvestment contract instance. + + @dev All parameters are set during construction and cannot be modified afterwards. + + @param someInvestor The address of the investor who is making the investment. + @param newInvestmentAmount The amount of Wei invested by the investor. Must be greater than or equal to 1 Wei. + @param tier The Tier object containing investment details like multiplier and lockin period. + */ constructor(address someInvestor, uint256 newInvestmentAmount, Tier tier) public { require(newInvestmentAmount >= 1, "A new investment amount should at least be 1."); _owner = msg.sender; @@ -58,29 +62,54 @@ contract TierInvestment is Interface { } /** - * Public counterpart of the _addPayee function, to add users that can withdraw - * funds after constructor initialisation. - */ + @notice Sets the remaining return amount for the investor for whom this TierInvestment was made. + @dev This function allows the owner of the TierInvestment object to set the remaining return amount for a specific + investor. It subtracts the newly returned amount from the remaining return balance. + @param someInvestor The address of the investor for whom the remaining return amount is being set. + @param newlyReturnedAmount The amount newly returned by the investor. + */ function publicSetRemainingReturn(address someInvestor, uint256 newlyReturnedAmount) public override onlyOwner { require(_investor == someInvestor, "Error, the new return is being set for the wrong investor."); _remainingReturn = _remainingReturn - newlyReturnedAmount; } + /** + @notice Retrieves the address of the investor associated with this TierInvestment object. + @dev This function is a view function that returns the address of the investor associated with this TierInvestment + object. + @return investor The address of the investor. + */ function getInvestor() public view override returns (address investor) { investor = _investor; return investor; } + /** + @notice Retrieves investment amount associated with this TierInvestment object. + @dev This function is a view function that returns the investment amount associated with this TierInvestment object. + @return newInvestmentAmount The new investment amount. + */ function getNewInvestmentAmount() public view override returns (uint256 newInvestmentAmount) { newInvestmentAmount = _newInvestmentAmount; return newInvestmentAmount; } + /** + @notice Retrieves the remaining return amount that the investor can still get with this TierInvestment object. + @dev This function is a view function that returns the remaining return that the investor can still get with this + TierInvestment object. + @return remainingReturn The remaining return amount. + */ function getRemainingReturn() public view override returns (uint256 remainingReturn) { remainingReturn = _remainingReturn; return remainingReturn; } + /** + @notice Retrieves the address of the owner of this contract. + @dev This function is a view function that returns the address of the owner of this contract. + @return The address of the owner. + */ function getOwner() public view override returns (address) { return _owner; } diff --git a/src/WorkerGetReward.sol b/src/WorkerGetReward.sol index cd988d2..b97b2af 100644 --- a/src/WorkerGetReward.sol +++ b/src/WorkerGetReward.sol @@ -21,10 +21,15 @@ contract WorkerGetReward is Interface { mapping(address => uint256) private _rewards; /** - * Constructor for creating a Tier instance. The values cannot be changed - * after creation. - * - */ + @notice This function is the constructor used to create a new WorkerGetReward contract instance. This contract + enables the project leader to allow workers to retrieve their payouts. + + @dev All parameters are set during construction and cannot be modified afterwards. + + @param projectLead The address of the project lead who can recover unclaimed rewards after a minimum duration. + @param minRetrievalDuration The minimum duration a worker must wait before they can claim their rewards from the + project lead. + */ constructor(address projectLead, uint256 minRetrievalDuration) public { _projectLead = projectLead; _minRetrievalDuration = minRetrievalDuration; @@ -33,8 +38,20 @@ contract WorkerGetReward is Interface { } /** - TODO: add duration to set minimum projectLead Recover fund date. Ensure project lead cannot - retrieve funds any earlier. */ + @notice This function allows the project lead to enable a worker to retrieve its payout. + + @dev The project lead must send Wei with the transaction and set a retrieval duration greater than or equal to the + minimum retrieval duration set during construction. The retrieval duration determines how long a worker can wait + before they claim their rewards from the project lead. + This function also updates the project lead's earliest retrieval time if the new reward duration extends beyond + the current time. + + This duration is to prevent funds from being locked up of a worker decides against picking up funds, e.g. because of + tax reasons, losing credentials, or passing away. + + @param worker The address of the worker to be rewarded. + @param retrievalDuration The amount of time (in seconds) the worker must wait before claiming their reward. + */ function addWorkerReward(address worker, uint256 retrievalDuration) public payable override { require(msg.value > 0, "Tried to add 0 value to worker reward."); require(retrievalDuration >= _minRetrievalDuration, "Tried to set retrievalDuration below min."); @@ -45,7 +62,13 @@ contract WorkerGetReward is Interface { } /** - TODO: ensure the worker cannot retrieve funds twice, and test it. */ + @notice This function allows a worker to retrieve their accumulated rewards. + + @dev A worker can only claim an amount up to their total accumulated rewards and what the contract currently holds. + The function performs a transfer and checks the balance change to validate success. + + @param amount The amount of Wei the worker wishes to claim. + */ function retreiveWorkerReward(uint256 amount) public override { require(amount > 0, "Amount not larger than 0."); require(_rewards[msg.sender] >= amount, "Asked more reward than worker can get."); @@ -58,6 +81,16 @@ contract WorkerGetReward is Interface { require(afterBalance - beforeBalance == amount, "Worker reward not transferred successfully."); } + /** + @notice This function allows the project lead to recover any unclaimed rewards after the minimum retrieval duration + has passed. + + @dev The project lead can only recover a non-zero amount up to the contract's current balance, and only after the + pre-defined wait time has elapsed, ensuring workers have had a chance to claim their rewards first. The wait time is + always the longest wait time required to facilitate the worker latest reward retrieval. + + @param amount The amount of Wei the project lead wishes to recover. + */ function projectLeadRecoversRewards(uint256 amount) public override { require(msg.sender == _projectLead, "Someone other than projectLead tried to recover rewards."); require(amount > 0, "Tried to recover 0 wei."); @@ -69,6 +102,14 @@ contract WorkerGetReward is Interface { payable(_projectLead).transfer(amount); } + /** + @notice This function retrieves the timestamp at which the project lead can first recover unclaimed rewards. + + @dev This initial value is set during construction and is later updated by the maximum time at which any worker can + still retrieve its reward. + + @return _projectLeadCanRecoverFrom The timestamp (in seconds since epoch) at which the project lead can recover funds. + */ function getProjectLeadCanRecoverFromTime() public view override returns (uint256) { return _projectLeadCanRecoverFrom; } diff --git a/test/integration/ComputeRemainingInvestorPayout.t.sol b/test/integration/ComputeRemainingInvestorPayout.t.sol index c4482f1..4474df9 100644 --- a/test/integration/ComputeRemainingInvestorPayout.t.sol +++ b/test/integration/ComputeRemainingInvestorPayout.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.23 <0.9.0; import { PRBTest } from "@prb/test/src/PRBTest.sol"; import { StdCheats } from "forge-std/src/StdCheats.sol"; -import { DecentralisedInvestmentHelper } from "../../src/Helper.sol"; +import { Helper } from "../../src/Helper.sol"; interface Interface { function setUp() external; @@ -21,12 +21,12 @@ interface Interface { } contract ComputeRemainingInvestorPayoutTest is PRBTest, StdCheats, Interface { - DecentralisedInvestmentHelper private _helper; + Helper private _helper; /// @dev A function invoked before each test case is run. function setUp() public override { // Initialise contract helper. - _helper = new DecentralisedInvestmentHelper(); + _helper = new Helper(); } /** diff --git a/test/unit/AcceptOffer.t.sol b/test/unit/AcceptOffer.t.sol index 376f658..60a0599 100644 --- a/test/unit/AcceptOffer.t.sol +++ b/test/unit/AcceptOffer.t.sol @@ -93,6 +93,7 @@ contract MultipleInvestmentTest is PRBTest, StdCheats, Interface { vm.warp(block.timestamp + 15 weeks); + vm.prank(_projectLeadAddress); _dim.triggerReturnAll(); assertEq(address(_dim).balance, 0 ether, "The _dim did not contain 0 ether."); } diff --git a/test/unit/CounterOffer.test.sol b/test/unit/CounterOffer.test.sol index fd096f3..5427f53 100644 --- a/test/unit/CounterOffer.test.sol +++ b/test/unit/CounterOffer.test.sol @@ -8,7 +8,7 @@ import { TierInvestment } from "../../src/TierInvestment.sol"; import { Tier } from "../../src/Tier.sol"; import { DecentralisedInvestmentManager } from "../../src/DecentralisedInvestmentManager.sol"; import { SaasPaymentProcessor } from "../../src/SaasPaymentProcessor.sol"; -import { DecentralisedInvestmentHelper } from "../../src/Helper.sol"; +import { Helper } from "../../src/Helper.sol"; import { ReceiveCounterOffer } from "../../src/ReceiveCounterOffer.sol"; import { ExposedDecentralisedInvestmentManager } from "test/unit/ExposedDecentralisedInvestmentManager.sol"; @@ -38,7 +38,7 @@ contract CounterOfferTest is PRBTest, StdCheats, Interface { uint256 private _projectLeadFracNumerator; uint256 private _projectLeadFracDenominator; SaasPaymentProcessor private _saasPaymentProcessor; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; TierInvestment[] private _tierInvestments; ExposedDecentralisedInvestmentManager private _exposed_dim; address payable private _investorWallet1; diff --git a/test/unit/DecentralisedInvestmentManager.t.sol b/test/unit/DecentralisedInvestmentManager.t.sol index 97f87f6..28c4cd9 100644 --- a/test/unit/DecentralisedInvestmentManager.t.sol +++ b/test/unit/DecentralisedInvestmentManager.t.sol @@ -11,7 +11,7 @@ import { StdCheats } from "forge-std/src/StdCheats.sol"; import { DecentralisedInvestmentManager } from "../../src/DecentralisedInvestmentManager.sol"; import { ExposedDecentralisedInvestmentManager } from "test/unit/ExposedDecentralisedInvestmentManager.sol"; import { SaasPaymentProcessor } from "../../src/SaasPaymentProcessor.sol"; -import { DecentralisedInvestmentHelper } from "../../src/Helper.sol"; +import { Helper } from "../../src/Helper.sol"; import { TierInvestment } from "../../src/TierInvestment.sol"; import { CustomPaymentSplitter } from "../../src/CustomPaymentSplitter.sol"; @@ -54,7 +54,7 @@ contract DecentralisedInvestmentManagerTest is PRBTest, StdCheats, Interface { uint256 private _projectLeadFracNumerator; uint256 private _projectLeadFracDenominator; SaasPaymentProcessor private _saasPaymentProcessor; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; TierInvestment[] private _tierInvestments; ExposedDecentralisedInvestmentManager private _exposed_dim; address payable private _investorWallet1; @@ -241,7 +241,7 @@ contract DecentralisedInvestmentManagerTest is PRBTest, StdCheats, Interface { function testPerformSaasRevenueAllocation() public override { _saasPaymentProcessor = new SaasPaymentProcessor(); - _helper = new DecentralisedInvestmentHelper(); + _helper = new Helper(); uint256 amountAboveContractBalance = 1; address receivingWallet = address(0); @@ -257,7 +257,7 @@ contract DecentralisedInvestmentManagerTest is PRBTest, StdCheats, Interface { function testPerformSaasRevenueAllocationToNonPayee() public override { _saasPaymentProcessor = new SaasPaymentProcessor(); - _helper = new DecentralisedInvestmentHelper(); + _helper = new Helper(); address receivingWallet = address(0); deal(address(_exposed_dim), 20); diff --git a/test/unit/RaisePeriod.t.sol b/test/unit/RaisePeriod.t.sol index 363e668..61bd434 100644 --- a/test/unit/RaisePeriod.t.sol +++ b/test/unit/RaisePeriod.t.sol @@ -98,11 +98,16 @@ contract MultipleInvestmentTest is PRBTest, StdCheats, Interface { vm.warp(block.timestamp + 3 weeks); vm.expectRevert(bytes("The fund raising period has not passed yet.")); + vm.prank(_projectLeadAddress); _dim.triggerReturnAll(); assertEq(address(_dim).balance, 0.5 ether, "The _dim did not contain 0.5 ether."); vm.warp(block.timestamp + 15 weeks); + vm.expectRevert(bytes("Someone other than projectLead tried to return all investments.")); + _dim.triggerReturnAll(); + + vm.prank(_projectLeadAddress); _dim.triggerReturnAll(); assertEq(address(_dim).balance, 0 ether, "The _dim did not contain 0 ether."); } diff --git a/test/unit/ReceiveAcceptedOffer.t.sol b/test/unit/ReceiveAcceptedOffer.t.sol index 6ee6022..d87da29 100644 --- a/test/unit/ReceiveAcceptedOffer.t.sol +++ b/test/unit/ReceiveAcceptedOffer.t.sol @@ -8,7 +8,7 @@ import { TierInvestment } from "../../src/TierInvestment.sol"; import { Tier } from "../../src/Tier.sol"; import { DecentralisedInvestmentManager } from "../../src/DecentralisedInvestmentManager.sol"; import { SaasPaymentProcessor } from "../../src/SaasPaymentProcessor.sol"; -import { DecentralisedInvestmentHelper } from "../../src/Helper.sol"; +import { Helper } from "../../src/Helper.sol"; import { ReceiveCounterOffer } from "../../src/ReceiveCounterOffer.sol"; import { ExposedDecentralisedInvestmentManager } from "test/unit/ExposedDecentralisedInvestmentManager.sol"; @@ -30,7 +30,7 @@ contract ReveiveAcceptedOfferTest is PRBTest, StdCheats, Interface { uint256 private _projectLeadFracNumerator; uint256 private _projectLeadFracDenominator; SaasPaymentProcessor private _saasPaymentProcessor; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; TierInvestment[] private _tierInvestments; ExposedDecentralisedInvestmentManager private _exposed_dim; address payable private _investorWallet1; diff --git a/test/unit/SaasPaymentProcessor.t.sol b/test/unit/SaasPaymentProcessor.t.sol index 6a3764f..f64f245 100644 --- a/test/unit/SaasPaymentProcessor.t.sol +++ b/test/unit/SaasPaymentProcessor.t.sol @@ -6,7 +6,7 @@ import { StdCheats } from "forge-std/src/StdCheats.sol"; import { TierInvestment } from "../../src/TierInvestment.sol"; import { SaasPaymentProcessor } from "../../src/SaasPaymentProcessor.sol"; import { Tier } from "../../src/Tier.sol"; -import { DecentralisedInvestmentHelper } from "../../src/Helper.sol"; +import { Helper } from "../../src/Helper.sol"; interface Interface { function setUp() external; @@ -20,13 +20,13 @@ interface Interface { contract SaasPaymentProcessorTest is PRBTest, StdCheats, Interface { SaasPaymentProcessor private _saasPaymentProcessor; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; TierInvestment[] private _tierInvestments; /// @dev A function invoked before each test case is run. function setUp() public virtual override { _saasPaymentProcessor = new SaasPaymentProcessor(); - _helper = new DecentralisedInvestmentHelper(); + _helper = new Helper(); } function testOnlyOwnerTriggered() public virtual override { diff --git a/test/unit/WorkerGetReward/AddWorkerReward.t.sol b/test/unit/WorkerGetReward/AddWorkerReward.t.sol index da508f0..373874c 100644 --- a/test/unit/WorkerGetReward/AddWorkerReward.t.sol +++ b/test/unit/WorkerGetReward/AddWorkerReward.t.sol @@ -7,7 +7,7 @@ import { Tier } from "../../../src/Tier.sol"; import { DecentralisedInvestmentManager } from "../../../src/DecentralisedInvestmentManager.sol"; import { ExposedDecentralisedInvestmentManager } from "test/unit/ExposedDecentralisedInvestmentManager.sol"; import { SaasPaymentProcessor } from "../../../src/SaasPaymentProcessor.sol"; -import { DecentralisedInvestmentHelper } from "../../../src/Helper.sol"; +import { Helper } from "../../../src/Helper.sol"; import { TierInvestment } from "../../../src/TierInvestment.sol"; import { CustomPaymentSplitter } from "../../../src/CustomPaymentSplitter.sol"; import { WorkerGetReward } from "../../../src/WorkerGetReward.sol"; @@ -37,7 +37,7 @@ contract WorkerGetRewardTest is PRBTest, StdCheats, Interface { uint256 private _projectLeadFracNumerator; uint256 private _projectLeadFracDenominator; SaasPaymentProcessor private _saasPaymentProcessor; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; TierInvestment[] private _tierInvestments; ExposedDecentralisedInvestmentManager private _exposed_dim; address payable private _investorWallet1; diff --git a/test/unit/WorkerGetReward/ProjectLeadRecoversRewards.t.sol b/test/unit/WorkerGetReward/ProjectLeadRecoversRewards.t.sol index 3431027..c94b764 100644 --- a/test/unit/WorkerGetReward/ProjectLeadRecoversRewards.t.sol +++ b/test/unit/WorkerGetReward/ProjectLeadRecoversRewards.t.sol @@ -7,7 +7,7 @@ import { Tier } from "../../../src/Tier.sol"; import { DecentralisedInvestmentManager } from "../../../src/DecentralisedInvestmentManager.sol"; import { ExposedDecentralisedInvestmentManager } from "test/unit/ExposedDecentralisedInvestmentManager.sol"; import { SaasPaymentProcessor } from "../../../src/SaasPaymentProcessor.sol"; -import { DecentralisedInvestmentHelper } from "../../../src/Helper.sol"; +import { Helper } from "../../../src/Helper.sol"; import { TierInvestment } from "../../../src/TierInvestment.sol"; import { CustomPaymentSplitter } from "../../../src/CustomPaymentSplitter.sol"; import { WorkerGetReward } from "../../../src/WorkerGetReward.sol"; @@ -35,7 +35,7 @@ contract WorkerGetRewardTest is PRBTest, StdCheats, Interface { uint256 private _projectLeadFracNumerator; uint256 private _projectLeadFracDenominator; SaasPaymentProcessor private _saasPaymentProcessor; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; TierInvestment[] private _tierInvestments; ExposedDecentralisedInvestmentManager private _exposed_dim; address payable private _investorWallet1; diff --git a/test/unit/WorkerGetReward/RetrieveWorkerReward.t.sol b/test/unit/WorkerGetReward/RetrieveWorkerReward.t.sol index 19ef28d..90f8da5 100644 --- a/test/unit/WorkerGetReward/RetrieveWorkerReward.t.sol +++ b/test/unit/WorkerGetReward/RetrieveWorkerReward.t.sol @@ -7,7 +7,7 @@ import { Tier } from "../../../src/Tier.sol"; import { DecentralisedInvestmentManager } from "../../../src/DecentralisedInvestmentManager.sol"; import { ExposedDecentralisedInvestmentManager } from "test/unit/ExposedDecentralisedInvestmentManager.sol"; import { SaasPaymentProcessor } from "../../../src/SaasPaymentProcessor.sol"; -import { DecentralisedInvestmentHelper } from "../../../src/Helper.sol"; +import { Helper } from "../../../src/Helper.sol"; import { TierInvestment } from "../../../src/TierInvestment.sol"; import { CustomPaymentSplitter } from "../../../src/CustomPaymentSplitter.sol"; import { WorkerGetReward } from "../../../src/WorkerGetReward.sol"; @@ -29,7 +29,7 @@ contract WorkerGetRewardTest is PRBTest, StdCheats, Interface { uint256 private _projectLeadFracNumerator; uint256 private _projectLeadFracDenominator; SaasPaymentProcessor private _saasPaymentProcessor; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; TierInvestment[] private _tierInvestments; ExposedDecentralisedInvestmentManager private _exposed_dim; address payable private _investorWallet1; diff --git a/test/unit/helper.t.sol b/test/unit/helper.t.sol index 4bc31f1..e60b101 100644 --- a/test/unit/helper.t.sol +++ b/test/unit/helper.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.23 <0.9.0; import { PRBTest } from "@prb/test/src/PRBTest.sol"; import { StdCheats } from "forge-std/src/StdCheats.sol"; -import { DecentralisedInvestmentHelper } from "../../src/Helper.sol"; +import { Helper } from "../../src/Helper.sol"; import { TierInvestment } from "../../src/TierInvestment.sol"; import { Tier } from "../../src/Tier.sol"; @@ -38,7 +38,7 @@ contract HelperTest is PRBTest, StdCheats, Interface { Tier[] private _tiers; Tier[] private _someTiers; - DecentralisedInvestmentHelper private _helper; + Helper private _helper; /// @dev A function invoked before each test case is run. function setUp() public virtual override { @@ -59,7 +59,7 @@ contract HelperTest is PRBTest, StdCheats, Interface { _tiers.push(tier2); // Initialise contract helper. - _helper = new DecentralisedInvestmentHelper(); + _helper = new Helper(); } function testExceedInvestmentCeiling() public override {