|
| 1 | +// SPDX-License-Identifier: BUSL-1.1 |
| 2 | +pragma solidity 0.8.19; |
| 3 | + |
| 4 | +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| 5 | +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| 6 | +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; |
| 7 | +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; |
| 8 | +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
| 9 | +import "./PaymentSplitterStorage.sol"; |
| 10 | +import "../Errors/Errors.sol"; |
| 11 | + |
| 12 | +/** |
| 13 | + * @author Renzo Protocol |
| 14 | + * @title PaymentSplitter |
| 15 | + * @dev Handles native ETH payments to be split among recipients. |
| 16 | + * A list of payment addresses and their corresponding amount to be paid out are tracked. |
| 17 | + * As ETH payments come in, they are split among the recipients until the amount to be paid is completed. |
| 18 | + * After all recipients are paid, any new ETH is sent to the fallback address. |
| 19 | + * ERC20 tokens are simply forwarded to the fallback address and can be triggered by any address. |
| 20 | + * @notice . |
| 21 | + */ |
| 22 | +contract PaymentSplitter is |
| 23 | + Initializable, |
| 24 | + OwnableUpgradeable, |
| 25 | + ReentrancyGuardUpgradeable, |
| 26 | + PaymentSplitterStorageV1 |
| 27 | +{ |
| 28 | + using SafeERC20 for IERC20; |
| 29 | + |
| 30 | + event RecipientAdded(address recipient, uint256 amountOwed); |
| 31 | + event RecipientRemoved(address recipient, uint256 amountOwed); |
| 32 | + event RecipientAmountIncreased(address recipient, uint256 amountOwed, uint256 increaseAmount); |
| 33 | + event RecipientAmountDecreased(address recipient, uint256 amountOwed, uint256 decreaseAmount); |
| 34 | + event RecipientPaid(address recipient, uint256 amountPaid, bool success); |
| 35 | + |
| 36 | + uint256 private constant DUST_AMOUNT = 1_000_000 gwei; |
| 37 | + |
| 38 | + /// @dev Prevents implementation contract from being initialized. |
| 39 | + /// @custom:oz-upgrades-unsafe-allow constructor |
| 40 | + constructor() { |
| 41 | + _disableInitializers(); |
| 42 | + } |
| 43 | + |
| 44 | + /** |
| 45 | + * @notice Initializes the contract with initial vars |
| 46 | + * @dev Contract init |
| 47 | + * @param _fallbackPaymentAddress The address where all funds will be sent after recipients are fully paid |
| 48 | + */ |
| 49 | + function initialize(address _fallbackPaymentAddress) public initializer { |
| 50 | + // Initialize inherited classes |
| 51 | + __Ownable_init(); |
| 52 | + __ReentrancyGuard_init(); |
| 53 | + |
| 54 | + fallbackPaymentAddress = _fallbackPaymentAddress; |
| 55 | + } |
| 56 | + |
| 57 | + /** |
| 58 | + * @notice Recipient Length |
| 59 | + * @dev view function for getting recipient length |
| 60 | + * @return uint256 . |
| 61 | + */ |
| 62 | + function getRecipientLength() public view returns (uint256) { |
| 63 | + return recipients.length; |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * @notice Forwards all ERC20 tokens to the fallback address |
| 68 | + * @dev Can be called by any address |
| 69 | + * If specified token balance is zero, reverts |
| 70 | + * Note that this is just a convenience function to handle any ERC20 tokens accidentally sent to this address, |
| 71 | + * and this is not expected to be used in normal operation |
| 72 | + * @param token IERC20 that was sent to this contract |
| 73 | + */ |
| 74 | + function forwardERC20(IERC20 token) public { |
| 75 | + uint256 balance = token.balanceOf(address(this)); |
| 76 | + if (balance == 0) { |
| 77 | + revert InvalidTokenReceived(); |
| 78 | + } |
| 79 | + |
| 80 | + token.safeTransfer(fallbackPaymentAddress, balance); |
| 81 | + } |
| 82 | + |
| 83 | + /** |
| 84 | + * @notice Adds a recipient and amount owed to the list |
| 85 | + * @dev Only callable by the owner |
| 86 | + * Any new payments coming in should start to be forwarded to the new recipient after this call |
| 87 | + * Cannot add the same recipient twice |
| 88 | + * @param _recipient Recipient address to add |
| 89 | + * @param _initialAmountOwed Initial amount owed to the recipient - can be set to 0 |
| 90 | + */ |
| 91 | + function addRecipient(address _recipient, uint256 _initialAmountOwed) public onlyOwner { |
| 92 | + // First iterate over the list to check to ensure the recipient is not already in the list |
| 93 | + for (uint256 i = 0; i < recipients.length; i++) { |
| 94 | + if (recipients[i] == _recipient) { |
| 95 | + revert AlreadyAdded(); |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + // Push to the list and set the amount owed |
| 100 | + recipients.push(_recipient); |
| 101 | + amountOwed[_recipient] = _initialAmountOwed; |
| 102 | + |
| 103 | + // Emit the event |
| 104 | + emit RecipientAdded(_recipient, _initialAmountOwed); |
| 105 | + } |
| 106 | + |
| 107 | + /** |
| 108 | + * @notice Removes a recipient from the list of payout addresses |
| 109 | + * @dev Only callable by the owner |
| 110 | + * Any new payments coming after this will not get forwarded to this recipient |
| 111 | + * If the recipient is not in the list, reverts |
| 112 | + * @param _recipient Recipient address to remove |
| 113 | + */ |
| 114 | + function removeRecipient(address _recipient) public onlyOwner { |
| 115 | + // First iterate over the list to check to ensure the recipient is in the list |
| 116 | + for (uint256 i = 0; i < recipients.length; i++) { |
| 117 | + if (recipients[i] == _recipient) { |
| 118 | + emit RecipientRemoved(_recipient, amountOwed[_recipient]); |
| 119 | + |
| 120 | + // Remove the recipient from the list |
| 121 | + recipients[i] = recipients[recipients.length - 1]; |
| 122 | + recipients.pop(); |
| 123 | + delete amountOwed[_recipient]; |
| 124 | + return; |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + revert NotFound(); |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * @notice Increases the amount owed to a recipient |
| 133 | + * @dev Only callable by the owner |
| 134 | + * If the recipient is not in the list, reverts |
| 135 | + * @param _recipient Recipient address to increase the amount owed |
| 136 | + * @param _amount Amount to add to outstanding balance owed |
| 137 | + */ |
| 138 | + function addToRecipientAmountOwed(address _recipient, uint256 _amount) public onlyOwner { |
| 139 | + // Iterate over the recipient list to find the recipient |
| 140 | + for (uint256 i = 0; i < recipients.length; i++) { |
| 141 | + if (recipients[i] == _recipient) { |
| 142 | + amountOwed[_recipient] += _amount; |
| 143 | + |
| 144 | + // Emit event |
| 145 | + emit RecipientAmountIncreased(_recipient, amountOwed[_recipient], _amount); |
| 146 | + |
| 147 | + return; |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + revert NotFound(); |
| 152 | + } |
| 153 | + |
| 154 | + /** |
| 155 | + * @notice Decreases the amount owed to a recipient |
| 156 | + * @dev Only callable by the owner |
| 157 | + * If the recipient is not in the list, reverts |
| 158 | + * If the amount to decrease is greater than the amount owed, sets amount owed to 0 |
| 159 | + * @param _recipient Recipient address to decrease the amount owed |
| 160 | + * @param _amount Amount to subtract from the outstanding balance owed |
| 161 | + */ |
| 162 | + function subtractFromRecipientAmountOwed(address _recipient, uint256 _amount) public onlyOwner { |
| 163 | + // Iterate over the recipient list to find the recipient |
| 164 | + for (uint256 i = 0; i < recipients.length; i++) { |
| 165 | + if (recipients[i] == _recipient) { |
| 166 | + // Check for higher amount to decrease than amount owed |
| 167 | + if (_amount >= amountOwed[_recipient]) { |
| 168 | + // Just set to 0 if the amount to decrease is greater than or equal to the amount owed |
| 169 | + amountOwed[_recipient] = 0; |
| 170 | + } else { |
| 171 | + // Subtract the amount from the amount owed |
| 172 | + amountOwed[_recipient] -= _amount; |
| 173 | + } |
| 174 | + |
| 175 | + // Emit event |
| 176 | + emit RecipientAmountDecreased(_recipient, amountOwed[_recipient], _amount); |
| 177 | + |
| 178 | + return; |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + revert NotFound(); |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * @notice Pay out the recipients when ETH comes in |
| 187 | + * @dev Any new payments coming in will be split among the recipients |
| 188 | + * Allows dust to be sent to recipients, but not to fallback address |
| 189 | + * Non Reentrant |
| 190 | + */ |
| 191 | + receive() external payable nonReentrant { |
| 192 | + // Always use the balance of the address in case there was a rounding error or leftover amount from the last payout |
| 193 | + uint256 amountLeftToPay = address(this).balance; |
| 194 | + if (amountLeftToPay == 0) { |
| 195 | + return; |
| 196 | + } |
| 197 | + |
| 198 | + // Iterate over the recipients and pay them out |
| 199 | + for (uint256 i = 0; i < recipients.length; i++) { |
| 200 | + // First get the amount to pay this recipient based on the number of payment addresses left in the list |
| 201 | + uint256 amountToPay = amountLeftToPay / (recipients.length - i); |
| 202 | + |
| 203 | + // Check if the amount owed is less than the amount to pay |
| 204 | + if (amountOwed[recipients[i]] < amountToPay) { |
| 205 | + amountToPay = amountOwed[recipients[i]]; |
| 206 | + } |
| 207 | + |
| 208 | + // Continue to the next one if the amount to pay is zero |
| 209 | + if (amountToPay == 0) { |
| 210 | + continue; |
| 211 | + } |
| 212 | + |
| 213 | + // Send the funds but ignore the return value to prevent others from not being paid |
| 214 | + (bool success, ) = recipients[i].call{ value: amountToPay }(""); |
| 215 | + |
| 216 | + // If successful update the amount owed and the amount left to pay |
| 217 | + if (success) { |
| 218 | + // Subtract the amount sent to the amount owed |
| 219 | + amountOwed[recipients[i]] -= amountToPay; |
| 220 | + |
| 221 | + // Subtract the amount sent from the total amount left to pay to other addresses |
| 222 | + amountLeftToPay -= amountToPay; |
| 223 | + |
| 224 | + // Track the total paid out to this recipient |
| 225 | + totalAmountPaid[recipients[i]] += amountToPay; |
| 226 | + } |
| 227 | + |
| 228 | + // Emit event |
| 229 | + emit RecipientPaid(recipients[i], amountToPay, success); |
| 230 | + } |
| 231 | + |
| 232 | + // If there is any amount left to pay, send it to the fallback address |
| 233 | + // ignore dust amounts due to division rounding or small left over amounts - they will get sent the next time this function is called |
| 234 | + if (amountLeftToPay > DUST_AMOUNT) { |
| 235 | + // Send the funds but ignore the return value to prevent others from not being paid |
| 236 | + (bool success, ) = fallbackPaymentAddress.call{ value: amountLeftToPay }(""); |
| 237 | + |
| 238 | + // If success, track the amount paid to the fallback |
| 239 | + if (success) { |
| 240 | + totalAmountPaid[fallbackPaymentAddress] += amountLeftToPay; |
| 241 | + } |
| 242 | + |
| 243 | + // Emit event |
| 244 | + emit RecipientPaid(fallbackPaymentAddress, amountLeftToPay, success); |
| 245 | + } |
| 246 | + } |
| 247 | +} |
0 commit comments