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