Skip to content

Commit 2aad4e6

Browse files
committed
add: payment splitter contracts
1 parent ce2718f commit 2aad4e6

File tree

2 files changed

+263
-0
lines changed

2 files changed

+263
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity 0.8.19;
3+
4+
abstract contract PaymentSplitterStorageV1 {
5+
/// @dev the address to send funds once recipients are paid
6+
address public fallbackPaymentAddress;
7+
8+
/// @dev tracks the total amount paid out to specific addresses
9+
mapping(address => uint256) public totalAmountPaid;
10+
11+
/// @dev tracks the amount owed to specific addresses
12+
mapping(address => uint256) public amountOwed;
13+
14+
/// @dev list of addresses to pay out for iteration
15+
address[] public recipients;
16+
}

0 commit comments

Comments
 (0)