// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; import "./interfaces/IERC20.sol"; import "./interfaces/IGovernance.sol"; import "./ReentrancyGuard.sol"; import "./interfaces/ISablierAirdrop.sol"; import "./interfaces/IRecipientStorage.sol"; import "./libraries/Types.sol"; contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard { /*** Storage Properties ***/ /** * @notice Tornado Governance contract is an owner of airdrop contract and can create or cancel airdrops */ address public constant tornadoGovernance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; /** * @notice TORN token - token for airdrops */ IERC20 public constant torn = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); /** * @notice Counter for new stream ids. */ uint256 public nextStreamId; /** * @notice The stream objects identifiable by their unsigned integer ids. */ mapping(uint256 => Types.Stream) private streams; /*** Modifiers ***/ /** * @dev Throws if the caller is not the recipient of the stream. */ modifier onlyRecipient(uint256 streamId) { require(msg.sender == streams[streamId].recipient, "caller is not the recipient of the stream"); _; } /** * @dev Throws if the caller is not Tornado Cash Governance contract */ modifier onlyGovernance() { require(msg.sender == tornadoGovernance, "caller is not governance"); _; } /** * @dev Throws if the provided id does not point to a valid stream. */ modifier streamExists(uint256 streamId) { require(streams[streamId].isEntity, "stream does not exist"); _; } /*** Contract Logic Starts Here */ constructor() { nextStreamId = 1; } /*** View Functions ***/ /** * @notice Returns the stream with all its properties. * @dev Throws if the id does not point to a valid stream. * @param streamId The id of the stream to query. */ function getStream( uint256 streamId ) external view streamExists(streamId) returns ( address recipient, uint256 deposit, uint256 startTime, uint256 stopTime, uint256 remainingBalance, uint256 ratePerSecond ) { recipient = streams[streamId].recipient; deposit = streams[streamId].deposit; startTime = streams[streamId].startTime; stopTime = streams[streamId].stopTime; remainingBalance = streams[streamId].remainingBalance; ratePerSecond = streams[streamId].ratePerSecond; } /** * @notice Returns either the delta in seconds between `block.timestamp` and `startTime` or * between `stopTime` and `startTime, whichever is smaller. If `block.timestamp` is before * `startTime`, it returns 0. * @dev Throws if the id does not point to a valid stream. * @param streamId The id of the stream for which to query the delta. * @return delta The time delta in seconds. */ function deltaOf(uint256 streamId) public view streamExists(streamId) returns (uint256 delta) { Types.Stream memory stream = streams[streamId]; if (block.timestamp <= stream.startTime) return 0; if (block.timestamp < stream.stopTime) return block.timestamp - stream.startTime; return stream.stopTime - stream.startTime; } /** * @notice Returns the available funds for the given stream id and address. * @dev Throws if the id does not point to a valid stream. * @param streamId The id of the stream for which to query the balance. * @param who The address for which to query the balance. * @return balance The total funds allocated to `who` as uint256. */ function balanceOf(uint256 streamId, address who) public view streamExists(streamId) returns (uint256 balance) { Types.Stream memory stream = streams[streamId]; uint256 delta = deltaOf(streamId); uint256 recipientBalance = delta * stream.ratePerSecond; /* * If the stream `balance` does not equal `deposit`, it means there have been withdrawals. * We have to subtract the total amount withdrawn from the amount of money that has been * streamed until now. */ if (stream.deposit > stream.remainingBalance) { uint256 withdrawalAmount = stream.deposit - stream.remainingBalance; recipientBalance = recipientBalance - withdrawalAmount; } if (who == stream.recipient) return recipientBalance; return 0; } /** * @notice To avoid "stack to deep" error */ struct CreateAirdropLocalVars { uint256 airdropDuration; uint256 ratePerSecond; uint256 firstStream; Types.Recipient[] airdropRecipients; } /*** Public Effects & Interactions Functions ***/ function createAirdrop( uint256 startTime, uint256 stopTime, address recipientStorage ) external onlyGovernance returns (bool) { CreateAirdropLocalVars memory vars; vars.airdropDuration = stopTime - startTime; vars.airdropRecipients = IRecipientStorage(recipientStorage).getAirdropRecipients(); vars.firstStream = nextStreamId; require(vars.airdropRecipients.length > 0, "no airdrop recipients"); for (uint256 i = 0; i < vars.airdropRecipients.length; i++) { uint256 normalizedDeposit = vars.airdropRecipients[i].deposit - (vars.airdropRecipients[i].deposit % vars.airdropDuration); address recipientAddr = vars.airdropRecipients[i].addr; uint256 recipientInitialLockedBalance = vars.airdropRecipients[i].initialLockedBalance; /* Without this, the rate per second would be zero. */ require(normalizedDeposit >= vars.airdropDuration, "deposit smaller than time delta"); vars.ratePerSecond = normalizedDeposit / vars.airdropDuration; /* Create and store the stream object. */ uint256 streamId = nextStreamId; streams[streamId] = Types.Stream({ remainingBalance: normalizedDeposit, deposit: normalizedDeposit, initialLockedBalance: recipientInitialLockedBalance, isEntity: true, ratePerSecond: vars.ratePerSecond, recipient: recipientAddr, startTime: startTime, stopTime: stopTime }); /* Increment the next stream id. */ nextStreamId = nextStreamId + 1; emit CreateStream( streamId, recipientAddr, normalizedDeposit, recipientInitialLockedBalance, startTime, stopTime ); } emit CreateAirdrop(startTime, stopTime, vars.airdropRecipients.length, vars.firstStream, nextStreamId - 1); return true; } /** * @notice Withdraws from the contract to the recipient's account all available recipient stream balance. * @dev Throws if the id does not point to a valid stream. * Throws if the caller is not the sender or the recipient of the stream. * Throws if the amount exceeds the available balance. * Throws if there is a token transfer failure. * @param streamId The id of the stream to withdraw tokens from. */ function withdrawFromStream( uint256 streamId ) external nonReentrant streamExists(streamId) onlyRecipient(streamId) returns (bool) { Types.Stream memory stream = streams[streamId]; uint256 balance = balanceOf(streamId, stream.recipient); require(balance > 0, "amount is zero"); uint256 recipientLockedBalance = IGovernance(tornadoGovernance).lockedBalance(stream.recipient); require(recipientLockedBalance >= stream.initialLockedBalance, "not enough locked tokens in governance"); /* Remaining balance can not be less than recipient stream balance */ streams[streamId].remainingBalance = stream.remainingBalance - balance; if (streams[streamId].remainingBalance == 0) delete streams[streamId]; torn.transfer(stream.recipient, balance); emit WithdrawFromStream(streamId, stream.recipient, balance); return true; } /** * @notice Cancels the stream and transfers the tokens back (remaining and unclaimed). * @dev Throws if the id does not point to a valid stream. * Throws if the caller is not the sender of the stream. * Throws if there is a token transfer failure. * @param streamId The id of the stream to cancel. * @return bool true=success, otherwise false. */ function cancelStream(uint256 streamId) external streamExists(streamId) onlyGovernance returns (bool) { Types.Stream memory stream = streams[streamId]; uint256 remainingBalance = stream.remainingBalance; delete streams[streamId]; if (remainingBalance > 0) torn.transfer(tornadoGovernance, remainingBalance); emit CancelStream(streamId, stream.recipient, remainingBalance); return true; } /** * @notice Cancels all airdrop streams and withdraw all unclaimed tokens back to governance. * @dev Throws if there is a token transfer failure. * @return bool true=success, otherwise false. */ function cancelAirdrop(uint256 firstStreamId, uint256 lastStreamId) external onlyGovernance returns (bool) { require(lastStreamId < nextStreamId, "last id exceeds stream count"); uint256 airdropRemainingBalance; for (uint256 streamId = firstStreamId; streamId <= lastStreamId; streamId++) { Types.Stream memory stream = streams[streamId]; uint256 remainingBalance = stream.remainingBalance; if (remainingBalance > 0) { airdropRemainingBalance += stream.remainingBalance; delete streams[streamId]; emit CancelStream(streamId, stream.recipient, stream.remainingBalance); } } torn.transfer(tornadoGovernance, airdropRemainingBalance); return true; } /** * @notice Withdraw all unclaimed tokens back to governance without canceling airdrop streams * @dev Throws if there is a token transfer failure. * @return bool true=success, otherwise false. */ function withdrawFunds() external onlyGovernance returns (bool) { torn.transfer(tornadoGovernance, torn.balanceOf(address(this))); return true; } }