proposal-47/contracts/Airdrop.sol

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;
}
}