proposal-47/contracts/Airdrop.sol
2024-01-31 12:04:44 +00:00

274 lines
10 KiB
Solidity

// 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 "./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;
}
/*** Public Effects & Interactions Functions ***/
struct CreateStreamLocalVars {
MathError mathErr;
uint256 duration;
uint256 ratePerSecond;
}
function createAirdrop(
Types.Recipient[] memory recipients,
uint256 startTime,
uint256 stopTime
) public onlyGovernance returns (bool) {
uint256 airdropDuration = stopTime - startTime;
CreateStreamLocalVars memory vars;
for (uint256 i = 0; i < recipients.length; i++) {
uint256 normalizedDeposit = recipients[i].deposit - (recipients[i].deposit % airdropDuration);
address recipientAddr = recipients[i].addr;
uint256 recipientInitialLockedBalance = recipients[i].initialLockedBalance;
/* Without this, the rate per second would be zero. */
require(normalizedDeposit >= airdropDuration, "deposit smaller than time delta");
(vars.mathErr, vars.ratePerSecond) = divUInt(normalizedDeposit, 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
);
}
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 nonReentrant 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 airdrop 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() external onlyGovernance returns (bool) {
torn.safeTransfer(tornadoGovernance, torn.balanceOf(address(this)));
return true;
}
}