// SPDX-License-Identifier: UNLICENSED pragma solidity =0.5.17; pragma experimental ABIEncoderV2; import "./interfaces/IERC20.sol"; import "./interfaces/IGovernance.sol"; import "./libraries/SafeERC20.sol"; import "./ReentrancyGuard.sol"; import "./CarefulMath.sol"; import "./interfaces/ISablierAirdrop.sol"; import "./interfaces/IRecipientStorage.sol"; import "./libraries/Types.sol"; contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard, CarefulMath { using SafeERC20 for IERC20; /*** Storage Properties ***/ address public constant tornadoGovernance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce; 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() public { 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. * @return The stream object. */ 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 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; } struct BalanceOfLocalVars { MathError mathErr; uint256 recipientBalance; uint256 withdrawalAmount; } /** * @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 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]; BalanceOfLocalVars memory vars; uint256 delta = deltaOf(streamId); (vars.mathErr, vars.recipientBalance) = mulUInt(delta, stream.ratePerSecond); require(vars.mathErr == MathError.NO_ERROR, "recipient balance calculation error"); /* * 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) { (vars.mathErr, vars.withdrawalAmount) = subUInt(stream.deposit, stream.remainingBalance); assert(vars.mathErr == MathError.NO_ERROR); (vars.mathErr, vars.recipientBalance) = subUInt(vars.recipientBalance, vars.withdrawalAmount); /* `withdrawalAmount` cannot and should not be bigger than `recipientBalance`. */ assert(vars.mathErr == MathError.NO_ERROR); } if (who == stream.recipient) return vars.recipientBalance; return 0; } /** * @notice To avoid "stack to deep" error */ struct CreateAirdropLocalVars { MathError mathErr; uint256 airdropDuration; uint256 ratePerSecond; uint256 firstStream; Types.Recipient[] airdropRecipients; } /*** Public Effects & Interactions Functions ***/ function createAirdrop( uint256 startTime, uint256 stopTime, address recipientStorage ) public 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.mathErr, vars.ratePerSecond) = divUInt(normalizedDeposit, vars.airdropDuration); /* `divUInt` can only return MathError.DIVISION_BY_ZERO but we know `duration` is not zero. */ assert(vars.mathErr == MathError.NO_ERROR); /* 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. */ (vars.mathErr, nextStreamId) = addUInt(nextStreamId, uint256(1)); require(vars.mathErr == MathError.NO_ERROR, "next stream id calculation error"); 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. * @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. * @param amount The amount of tokens to withdraw. */ function withdrawFromStream( uint256 streamId, uint256 amount ) external nonReentrant streamExists(streamId) onlyRecipient(streamId) returns (bool) { require(amount > 0, "amount is zero"); Types.Stream memory stream = streams[streamId]; uint256 recipientLockedBalance = IGovernance(tornadoGovernance).lockedBalance(stream.recipient); require(recipientLockedBalance >= stream.initialLockedBalance, "not enough locked tokens in governance"); uint256 balance = balanceOf(streamId, stream.recipient); require(balance >= amount, "amount exceeds the available balance"); MathError mathErr; (mathErr, streams[streamId].remainingBalance) = subUInt(stream.remainingBalance, amount); /** * `subUInt` can only return MathError.INTEGER_UNDERFLOW but we know that `remainingBalance` is at least * as big as `amount`. */ assert(mathErr == MathError.NO_ERROR); if (streams[streamId].remainingBalance == 0) delete streams[streamId]; torn.safeTransfer(stream.recipient, amount); emit WithdrawFromStream(streamId, stream.recipient, amount); 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.safeTransfer(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.safeTransfer(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.safeTransfer(tornadoGovernance, torn.balanceOf(address(this))); return true; } }