proposal-47/contracts/Airdrop.sol

286 lines
10 KiB
Solidity
Raw Permalink Normal View History

2024-01-31 15:04:44 +03:00
// SPDX-License-Identifier: UNLICENSED
2024-02-12 12:27:34 +03:00
pragma solidity ^0.8.9;
2024-01-31 15:04:44 +03:00
import "./interfaces/IERC20.sol";
import "./interfaces/IGovernance.sol";
import "./ReentrancyGuard.sol";
import "./interfaces/ISablierAirdrop.sol";
2024-02-10 21:43:54 +03:00
import "./interfaces/IRecipientStorage.sol";
2024-01-31 15:04:44 +03:00
import "./libraries/Types.sol";
2024-02-12 12:27:34 +03:00
contract SablierAirdrop is ISablierAirdrop, ReentrancyGuard {
2024-01-31 15:04:44 +03:00
/*** Storage Properties ***/
2024-02-12 12:27:34 +03:00
/**
* @notice Tornado Governance contract is an owner of airdrop contract and can create or cancel airdrops
*/
2024-01-31 15:04:44 +03:00
address public constant tornadoGovernance = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
2024-02-12 12:27:34 +03:00
/**
* @notice TORN token - token for airdrops
*/
2024-01-31 15:04:44 +03:00
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 */
2024-02-12 12:27:34 +03:00
constructor() {
2024-01-31 15:04:44 +03:00
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.
2024-02-12 12:27:34 +03:00
* @return delta The time delta in seconds.
2024-01-31 15:04:44 +03:00
*/
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.
2024-02-12 12:27:34 +03:00
* @return balance The total funds allocated to `who` as uint256.
2024-01-31 15:04:44 +03:00
*/
function balanceOf(uint256 streamId, address who) public view streamExists(streamId) returns (uint256 balance) {
Types.Stream memory stream = streams[streamId];
uint256 delta = deltaOf(streamId);
2024-02-12 12:27:34 +03:00
uint256 recipientBalance = delta * stream.ratePerSecond;
2024-01-31 15:04:44 +03:00
/*
* 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) {
2024-02-12 12:27:34 +03:00
uint256 withdrawalAmount = stream.deposit - stream.remainingBalance;
recipientBalance = recipientBalance - withdrawalAmount;
2024-01-31 15:04:44 +03:00
}
2024-02-12 12:27:34 +03:00
if (who == stream.recipient) return recipientBalance;
2024-01-31 15:04:44 +03:00
return 0;
}
2024-02-10 21:43:54 +03:00
/**
* @notice To avoid "stack to deep" error
*/
struct CreateAirdropLocalVars {
uint256 airdropDuration;
2024-01-31 15:04:44 +03:00
uint256 ratePerSecond;
2024-02-10 21:43:54 +03:00
uint256 firstStream;
Types.Recipient[] airdropRecipients;
2024-01-31 15:04:44 +03:00
}
2024-02-10 21:43:54 +03:00
/*** Public Effects & Interactions Functions ***/
2024-01-31 15:04:44 +03:00
function createAirdrop(
uint256 startTime,
2024-02-10 21:43:54 +03:00
uint256 stopTime,
address recipientStorage
2024-02-12 12:27:34 +03:00
) external onlyGovernance returns (bool) {
2024-02-10 21:43:54 +03:00
CreateAirdropLocalVars memory vars;
vars.airdropDuration = stopTime - startTime;
vars.airdropRecipients = IRecipientStorage(recipientStorage).getAirdropRecipients();
vars.firstStream = nextStreamId;
2024-01-31 15:04:44 +03:00
2024-02-10 21:43:54 +03:00
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;
2024-01-31 15:04:44 +03:00
/* Without this, the rate per second would be zero. */
2024-02-10 21:43:54 +03:00
require(normalizedDeposit >= vars.airdropDuration, "deposit smaller than time delta");
2024-01-31 15:04:44 +03:00
2024-02-12 12:27:34 +03:00
vars.ratePerSecond = normalizedDeposit / vars.airdropDuration;
2024-01-31 15:04:44 +03:00
/* 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. */
2024-02-12 12:27:34 +03:00
nextStreamId = nextStreamId + 1;
2024-01-31 15:04:44 +03:00
emit CreateStream(
streamId,
recipientAddr,
normalizedDeposit,
recipientInitialLockedBalance,
startTime,
stopTime
);
}
2024-02-10 21:43:54 +03:00
emit CreateAirdrop(startTime, stopTime, vars.airdropRecipients.length, vars.firstStream, nextStreamId - 1);
2024-01-31 15:04:44 +03:00
return true;
}
/**
2024-02-12 12:27:34 +03:00
* @notice Withdraws from the contract to the recipient's account all available recipient stream balance.
2024-01-31 15:04:44 +03:00
* @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(
2024-02-12 12:27:34 +03:00
uint256 streamId
2024-01-31 15:04:44 +03:00
) external nonReentrant streamExists(streamId) onlyRecipient(streamId) returns (bool) {
Types.Stream memory stream = streams[streamId];
2024-02-12 12:27:34 +03:00
uint256 balance = balanceOf(streamId, stream.recipient);
require(balance > 0, "amount is zero");
2024-01-31 15:04:44 +03:00
uint256 recipientLockedBalance = IGovernance(tornadoGovernance).lockedBalance(stream.recipient);
require(recipientLockedBalance >= stream.initialLockedBalance, "not enough locked tokens in governance");
2024-02-12 12:27:34 +03:00
/* Remaining balance can not be less than recipient stream balance */
streams[streamId].remainingBalance = stream.remainingBalance - balance;
2024-01-31 15:04:44 +03:00
if (streams[streamId].remainingBalance == 0) delete streams[streamId];
2024-02-12 12:27:34 +03:00
torn.transfer(stream.recipient, balance);
emit WithdrawFromStream(streamId, stream.recipient, balance);
2024-01-31 15:04:44 +03:00
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.
*/
2024-02-10 21:43:54 +03:00
function cancelStream(uint256 streamId) external streamExists(streamId) onlyGovernance returns (bool) {
2024-01-31 15:04:44 +03:00
Types.Stream memory stream = streams[streamId];
uint256 remainingBalance = stream.remainingBalance;
delete streams[streamId];
2024-02-12 12:27:34 +03:00
if (remainingBalance > 0) torn.transfer(tornadoGovernance, remainingBalance);
2024-01-31 15:04:44 +03:00
emit CancelStream(streamId, stream.recipient, remainingBalance);
return true;
}
/**
2024-02-10 21:43:54 +03:00
* @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);
}
}
2024-02-12 12:27:34 +03:00
torn.transfer(tornadoGovernance, airdropRemainingBalance);
2024-02-10 21:43:54 +03:00
return true;
}
/**
* @notice Withdraw all unclaimed tokens back to governance without canceling airdrop streams
2024-01-31 15:04:44 +03:00
* @dev Throws if there is a token transfer failure.
* @return bool true=success, otherwise false.
*/
2024-02-10 21:43:54 +03:00
function withdrawFunds() external onlyGovernance returns (bool) {
2024-02-12 12:27:34 +03:00
torn.transfer(tornadoGovernance, torn.balanceOf(address(this)));
2024-01-31 15:04:44 +03:00
return true;
}
}