286 lines
10 KiB
Solidity
286 lines
10 KiB
Solidity
// 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;
|
|
}
|
|
}
|