diff --git a/src/Grants4FirstBatch/Context.sol b/src/Grants4FirstBatch/Context.sol new file mode 100644 index 0000000..d76a7b2 --- /dev/null +++ b/src/Grants4FirstBatch/Context.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } +} diff --git a/src/Grants4FirstBatch/Grants4FirstBatch.sol b/src/Grants4FirstBatch/Grants4FirstBatch.sol new file mode 100644 index 0000000..9deaa6a --- /dev/null +++ b/src/Grants4FirstBatch/Grants4FirstBatch.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { Ownable2Step } from "./Ownable2Step.sol"; +import { Ownable } from "./Ownable.sol"; + +contract Grants4FirstBatch is Ownable2Step { + //////////////////////////////////////////////////////////////// + // ERRORS // + //////////////////////////////////////////////////////////////// + + /// @notice Error that is thrown if the caller is not allowed to call the function + error OnlyAllowedCaller(); + + /// @notice Error that is thrown if the input arrays have different lengths + error LengthMismatch(); + + //////////////////////////////////////////////////////////////// + // EVENTS // + //////////////////////////////////////////////////////////////// + + /// @notice Event emitted when a caller is added + event CallerAdded(address indexed caller); + + /// @notice Event emitted when a caller is removed + event CallerRemoved(address indexed caller); + + /// @notice Event emitted when the AllowanceModule is set + event AllowanceModuleSet(address indexed allowanceModule); + + /// @notice Event emitted when the WLD token is set + event WldTokenSet(address indexed wldToken); + + /// @notice Event emitted when the Holder is set + event HolderSet(address indexed holder); + + /// @notice Event emitted when the RecurringGrantDrop is set + event RecurringGrantDropSet(address indexed recurringGrantDrop); + + /// @notice Event emitted when a batch has been processed + event BatchProcessed(uint256 batchSize, uint256 successfulExecutions); + + //////////////////////////////////////////////////////////////// + /// CONFIG STORAGE /// + //////////////////////////////////////////////////////////////// + + /// @notice address of the Safe Allowance Module + AllowanceModule public ALLOWANCE_MODULE; + + /// @notice Worldcoin token address + address public WLD_TOKEN; + + /// @notice BVI Safe that grants allowances to this contract + GnosisSafe public HOLDER; + + /// @notice Recurring Grant Drop contract + IRecurringGrantDrop public RECURRING_GRANT_DROP; + + /// @notice addresses that can call the batch function + mapping(address => bool) public allowedCallers; + + //////////////////////////////////////////////////////////////// + /// CONSTRUCTOR /// + //////////////////////////////////////////////////////////////// + + constructor( + address _allowanceModuleAddress, + address _wldToken, + address _holder, + address _recurringGrantDrop + ) + Ownable(msg.sender) + { + ALLOWANCE_MODULE = AllowanceModule(_allowanceModuleAddress); + WLD_TOKEN = _wldToken; + HOLDER = GnosisSafe(_holder); + RECURRING_GRANT_DROP = IRecurringGrantDrop(_recurringGrantDrop); + } + + //////////////////////////////////////////////////////////////// + /// FUNCTIONS /// + //////////////////////////////////////////////////////////////// + + /// @notice Batch setNullifierHash + transfer WLD tokens to multiple addresses + /// @param _nullifierHashes array of nullifier hashes + /// @param _recipients array of recipient addresses + /// @param _amounts array of amounts to transfer + function batch( + uint256[] calldata _nullifierHashes, + address[] calldata _recipients, + uint256[] calldata _amounts + ) + external + { + if (!(_nullifierHashes.length == _recipients.length && _recipients.length == _amounts.length)) { + revert LengthMismatch(); + } + + if (!allowedCallers[msg.sender]) { + revert OnlyAllowedCaller(); + } + + uint256 batchSize = _nullifierHashes.length; + uint256 successfulExecutions = 0; + for (uint256 i = 0; i < _recipients.length; i++) { + try RECURRING_GRANT_DROP.setNullifierHash(_nullifierHashes[i]) { + AllowanceModule(ALLOWANCE_MODULE).executeAllowanceTransfer( + HOLDER, WLD_TOKEN, payable(_recipients[i]), uint96(_amounts[i]) + ); + successfulExecutions++; + } catch (bytes memory) { } + } + + emit BatchProcessed(batchSize, successfulExecutions); + } + + //////////////////////////////////////////////////////////////// + /// CONFIG FUNCTIONS /// + //////////////////////////////////////////////////////////////// + + function setAllowanceModule(address _allowanceModuleAddress) external onlyOwner { + ALLOWANCE_MODULE = AllowanceModule(_allowanceModuleAddress); + emit AllowanceModuleSet(_allowanceModuleAddress); + } + + function setWldToken(address _wldToken) external onlyOwner { + WLD_TOKEN = _wldToken; + emit WldTokenSet(_wldToken); + } + + function setHolder(address _holder) external onlyOwner { + HOLDER = GnosisSafe(_holder); + emit HolderSet(_holder); + } + + function setRecurringGrantDrop(IRecurringGrantDrop _recurringGrantDrop) external onlyOwner { + RECURRING_GRANT_DROP = _recurringGrantDrop; + emit RecurringGrantDropSet(address(_recurringGrantDrop)); + } + + function addCaller(address _caller) external onlyOwner { + allowedCallers[_caller] = true; + emit CallerAdded(_caller); + } + + function removeCaller(address _caller) external onlyOwner { + delete allowedCallers[_caller]; + emit CallerRemoved(_caller); + } +} + +interface GnosisSafe { + /// @dev Allows a Module to execute a Safe transaction without any further confirmations. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction. + function execTransactionFromModule( + address to, + uint256 value, + bytes calldata data, + Enum.Operation operation + ) + external + returns (bool success); +} + +contract Enum { + enum Operation { + Call, + DelegateCall + } +} + +interface IRecurringGrantDrop { + function setNullifierHash(uint256 nullifierHash) external; +} + +interface AllowanceModule { + function executeAllowanceTransfer(GnosisSafe safe, address token, address payable to, uint96 amount) external; +} diff --git a/src/Grants4FirstBatch/Ownable.sol b/src/Grants4FirstBatch/Ownable.sol new file mode 100644 index 0000000..5d47a25 --- /dev/null +++ b/src/Grants4FirstBatch/Ownable.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; +import { Context } from "./Context.sol"; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * The initial owner is set to the address provided by the deployer. This can + * later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + /** + * @dev The caller account is not authorized to perform an operation. + */ + error OwnableUnauthorizedAccount(address account); + + /** + * @dev The owner is not a valid owner account. (eg. `address(0)`) + */ + error OwnableInvalidOwner(address owner); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the address provided by the deployer as the initial owner. + */ + constructor(address initialOwner) { + if (initialOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(initialOwner); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + if (owner() != _msgSender()) { + revert OwnableUnauthorizedAccount(_msgSender()); + } + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby disabling any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} diff --git a/src/Grants4FirstBatch/Ownable2Step.sol b/src/Grants4FirstBatch/Ownable2Step.sol new file mode 100644 index 0000000..22bd1b6 --- /dev/null +++ b/src/Grants4FirstBatch/Ownable2Step.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import { Ownable } from "./Ownable.sol"; + +/** + * @dev Contract module which provides access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * The initial owner is specified at deployment time in the constructor for `Ownable`. This + * can later be changed with {transferOwnership} and {acceptOwnership}. + * + * This module is used through inheritance. It will make available all functions + * from parent (Ownable). + */ +abstract contract Ownable2Step is Ownable { + address private _pendingOwner; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Returns the address of the pending owner. + */ + function pendingOwner() public view virtual returns (address) { + return _pendingOwner; + } + + /** + * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is + * one. + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual override onlyOwner { + _pendingOwner = newOwner; + emit OwnershipTransferStarted(owner(), newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner. + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual override { + delete _pendingOwner; + super._transferOwnership(newOwner); + } + + /** + * @dev The new owner accepts the ownership transfer. + */ + function acceptOwnership() public virtual { + address sender = _msgSender(); + if (pendingOwner() != sender) { + revert OwnableUnauthorizedAccount(sender); + } + _transferOwnership(sender); + } +}