Skip to content

Commit

Permalink
Contract v1 (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
carletex authored Mar 14, 2024
1 parent ef9e2f0 commit da82300
Showing 1 changed file with 192 additions and 21 deletions.
213 changes: 192 additions & 21 deletions packages/hardhat/contracts/BGGrants.sol
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

// Dummy contract for initial iteration
// From https://github.com/BuidlGuidl/Eth-Splitter/blob/master/packages/hardhat/contracts/Splitter.sol
contract BGGrants is Ownable, ReentrancyGuard {
error INVALID_RECIPIENT();
interface ENSContract {
function setName(string memory newName) external;
}

/**
* @title BGGrants
* @notice A smart contract to split ETH or ERC20 tokens between multiple recipients.
* @dev This is intended for research and development purposes only. Use this contract at your
* own risk and discretion.
* @dev Based on https://split.buidlguidl.com/
*/

contract BGGrants is ReentrancyGuard, AccessControl {
using SafeERC20 for IERC20;
// Mainnet ENS contract
ENSContract public immutable ensContract = ENSContract(0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb);

bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

// Events
event EthSplit(address indexed sender, uint256 totalAmount, address payable[] recipients, uint256[] amounts);
event EthSplitEqual(address indexed sender, uint256 totalAmount, address payable[] recipients);
event Erc20Split(address indexed sender, address payable[] recipients, uint256[] amounts, IERC20 token);
event Erc20SplitEqual(address indexed sender, uint256 totalAmount, address payable[] recipients, IERC20 token);

//*********************************************************************//
// --------------------------- custom errors ------------------------- //
//*********************************************************************//
error INVALID_INPUT();
error INSUFFICIENT_RECIPIENT_COUNT();
error TRANSFER_FAILED();
error INVALID_RECIPIENT();
error INSUFFICIENT_SPLIT_AMOUNT();
error INVALID_INPUT();
error TRANSFER_FAILED();
error NOT_ADMIN();

event EthSplit(address indexed sender, uint256 totalAmount, address payable[] recipients, uint256[] amounts);
modifier onlyAdmin() {
if (!hasRole(ADMIN_ROLE, msg.sender)) revert NOT_ADMIN();
_;
}

/**
* @notice The constructor sets the owner of the contract
*/
constructor(address _owner) {
super.transferOwnership(_owner);
_setupRole(DEFAULT_ADMIN_ROLE, _owner);
_setupRole(ADMIN_ROLE, _owner);
}

/**
* @notice Splits the ETH amongst the given recipients, according to the specified amounts
* @param recipients The recipients of the ETH
* @param amounts The amounts each recipient shall receive
*/
function splitETH(
address payable[] calldata recipients,
uint256[] calldata amounts
) external payable nonReentrant {
) external payable nonReentrant onlyAdmin {
uint256 remainingAmount = _splitETH(recipients, amounts, msg.value);
emit EthSplit(msg.sender, msg.value, recipients, amounts);

Expand All @@ -32,6 +71,93 @@ contract BGGrants is Ownable, ReentrancyGuard {
}
}

/**
* @notice Splits the ETH equally amongst the given recipients
* @dev The contract adds any leftover dust to the amount to be received by the first recipient in the input array.
* @param recipients The recipients of the ETH
*/
function splitEqualETH(
address payable[] calldata recipients
) external payable nonReentrant onlyAdmin {
uint256 totalAmount = msg.value;
uint256 rLength = recipients.length;
uint256 equalAmount = totalAmount / rLength;
uint256 remainingAmount = totalAmount % rLength;

if (rLength > 25) revert INSUFFICIENT_RECIPIENT_COUNT();

for (uint256 i = 0; i < rLength; ) {
if (recipients[i] == address(0)) revert INVALID_RECIPIENT();
uint256 amountToSend = equalAmount;
if (i == 0) {
amountToSend = amountToSend + remainingAmount;
}
(bool success, ) = recipients[i].call{value: amountToSend, gas: 20000}("");
if (!success) revert TRANSFER_FAILED();
unchecked {
++i;
}
}

emit EthSplitEqual(msg.sender, msg.value, recipients);
}

/**
* @notice Splits the ERC20 tokens amongst the given recipients, according to the specified amounts
* @param token The token to be shared amongst the recipients
* @param recipients The recipients of the ERC20 tokens
* @param amounts The amounts each recipient shall receive
*/
function splitERC20(
IERC20 token,
address payable[] calldata recipients,
uint256[] calldata amounts
) external nonReentrant onlyAdmin {
_transferTokensFromSenderToRecipients(token, recipients, amounts);
emit Erc20Split(msg.sender, recipients, amounts, token);
}

/**
* @notice Splits the ERC20 tokens equally amongst the given recipients
* @param token The token to be shared amongst the recipients
* @param recipients The recipients of the ERC20 tokens
* @param totalAmount The total amount to be shared
*/
function splitEqualERC20(
IERC20 token,
address payable[] calldata recipients,
uint256 totalAmount
) external nonReentrant onlyAdmin {
uint256 rLength = recipients.length;

if (rLength > 25) revert INSUFFICIENT_RECIPIENT_COUNT();

uint256 equalAmount = totalAmount / rLength;

uint256 remainingAmount = totalAmount % rLength;
for (uint256 i = 0; i < rLength; ) {
if (recipients[i] == address(0)) revert INVALID_RECIPIENT();

uint256 amountToSend = equalAmount;
if (i == 0) {
amountToSend = amountToSend + remainingAmount;
}
SafeERC20.safeTransferFrom(token, msg.sender, recipients[i], amountToSend);
unchecked {
++i;
}
}

emit Erc20SplitEqual(msg.sender, totalAmount, recipients, token);
}

/**
* @notice Internal function to split the ETH amongst the given recipients, according to the specified amounts
* @param recipients The recipients of the ETH
* @param amounts The amounts each recipient shall receive
* @param totalAvailable The total available ETH to be split
* @return remainingAmount The remaining ETH dust
*/
function _splitETH(
address payable[] calldata recipients,
uint256[] calldata amounts,
Expand Down Expand Up @@ -60,16 +186,61 @@ contract BGGrants is Ownable, ReentrancyGuard {
}

/**
* Function that allows the owner to withdraw all the Ether in the contract
* The function can only be called by the owner of the contract as defined by the isOwner modifier
*/
function withdraw() public onlyOwner {
(bool success, ) = owner().call{ value: address(this).balance }("");
require(success, "Failed to send Ether");
* @notice Internal function to transfer ERC20 tokens from the sender to the recipients
* @param erc20Token The ERC20 token to be shared
* @param recipients The noble recipients of the tokens
* @param amounts The amounts each recipient shall receive
*/
function _transferTokensFromSenderToRecipients(
IERC20 erc20Token,
address payable[] calldata recipients,
uint256[] calldata amounts
) internal {
uint256 length = recipients.length;

if (length != amounts.length) revert INVALID_INPUT();
if (length > 25) revert INSUFFICIENT_RECIPIENT_COUNT();

for (uint256 i = 0; i < length; ) {
if (recipients[i] == address(0)) revert INVALID_RECIPIENT();
if (amounts[i] == 0) revert INSUFFICIENT_SPLIT_AMOUNT();

SafeERC20.safeTransferFrom(erc20Token, msg.sender, recipients[i], amounts[i]);
unchecked {
++i;
}
}
}

function addAdmin(address _admin) external onlyAdmin {
grantRole(ADMIN_ROLE, _admin);
}

function removeAdmin(address _admin) external onlyAdmin {
revokeRole(ADMIN_ROLE, _admin);
}

/**
* Function that allows the contract to receive ETH
*/
* @notice Withdraws the remaining ETH or ERC20 tokens to the owner's address
* @param token The address of the ERC20 token, or 0 for ETH
*/
function withdraw(IERC20 token) external onlyAdmin {
if (address(token) == address(0)) {
(bool success, ) = msg.sender.call{value: address(this).balance, gas: 20000}("");
if (!success) revert TRANSFER_FAILED();
} else {
token.transfer(msg.sender, token.balanceOf(address(this)));
}
}

/**
* Set the reverse ENS name for the contract
* @param _newName The new ENS name for the contract
* @dev only meant to be call on mainnet
*/
function setName(string memory _newName) onlyAdmin public {
ensContract.setName(_newName);
}

receive() external payable {}
}

0 comments on commit da82300

Please sign in to comment.