Untitled Diff
283 lines
/**
/**
* @title Sablier
* @title AaveEcosystemReserve v2
* @author Sablier
* @notice Stores ERC20 tokens of an ecosystem reserve, adding streaming capabilities.
* @notice Money streaming.
* Modification of Sablier https://github.com/sablierhq/sablier/blob/develop/packages/protocol/contracts/Sablier.sol
*/
* Original can be found also deployed on https://etherscan.io/address/0xCD18eAa163733Da39c232722cBC4E8940b1D8888
contract Sablier is ISablier, ReentrancyGuard, CarefulMath {
* Modifications:
* - Sablier "pulls" the funds from the creator of the stream at creation. In the Aave case, we already have the funds.
* - Anybody can create streams on Sablier. Here, only the funds admin (Aave governance via controller) can
* - Adapted codebase to Solidity 0.8.11, mainly removing SafeMath and CarefulMath to use native safe math
* - Same as with creation, on Sablier the `sender` and `recipient` can cancel a stream. Here, only fund admin and recipient
* @author BGD Labs
**/
contract AaveEcosystemReserveV2 is
AdminControlledEcosystemReserve,
ReentrancyGuard,
IStreamable
{
using SafeERC20 for IERC20;
using SafeERC20 for IERC20;
/*** Storage Properties ***/
/*** Storage Properties ***/
/**
/**
* @notice Counter for new stream ids.
* @notice Counter for new stream ids.
*/
*/
uint256 public nextStreamId;
uint256 private _nextStreamId;
/**
/**
* @notice The stream objects identifiable by their unsigned integer ids.
* @notice The stream objects identifiable by their unsigned integer ids.
*/
*/
mapping(uint256 => Types.Stream) private streams;
mapping(uint256 => Stream) private _streams;
/*** Modifiers ***/
/*** Modifiers ***/
/**
/**
* @dev Throws if the caller is not the sender of the recipient of the stream.
* @dev Throws if the caller is not the funds admin of the recipient of the stream.
*/
*/
modifier onlySenderOrRecipient(uint256 streamId) {
modifier onlyAdminOrRecipient(uint256 streamId) {
require(
require(
msg.sender == streams[streamId].sender || msg.sender == streams[streamId].recipient,
msg.sender == _fundsAdmin ||
"caller is not the sender or the recipient of the stream"
msg.sender == _streams[streamId].recipient,
"caller is not the funds admin or the recipient of the stream"
);
);
_;
_;
}
}
/**
/**
* @dev Throws if the provided id does not point to a valid stream.
* @dev Throws if the provided id does not point to a valid stream.
*/
*/
modifier streamExists(uint256 streamId) {
modifier streamExists(uint256 streamId) {
require(streams[streamId].isEntity, "stream does not exist");
require(_streams[streamId].isEntity, "stream does not exist");
_;
_;
}
}
/*** Contract Logic Starts Here */
/*** Contract Logic Starts Here */
constructor() public {
function initialize(address fundsAdmin) external initializer {
nextStreamId = 100000;
_nextStreamId = 100000;
_setFundsAdmin(fundsAdmin);
}
}
/*** View Functions ***/
/*** View Functions ***/
/**
/**
* @notice Returns the next available stream id
* @notice Returns the stream id.
*/
function getNextStreamId() external view returns (uint256) {
return _nextStreamId;
}
/**
* @notice Returns the stream with all its properties.
* @notice Returns the stream with all its properties.
* @dev Throws if the id does not point to a valid stream.
* @dev Throws if the id does not point to a valid stream.
* @param streamId The id of the stream to query.
* @param streamId The id of the stream to query.
* @return The stream object.
* @notice Returns the stream object.
*/
*/
function getStream(uint256 streamId)
function getStream(uint256 streamId)
external
external
view
view
streamExists(streamId)
streamExists(streamId)
returns (
returns (
address sender,
address sender,
address recipient,
address recipient,
uint256 deposit,
uint256 deposit,
address tokenAddress,
address tokenAddress,
uint256 startTime,
uint256 startTime,
uint256 stopTime,
uint256 stopTime,
uint256 remainingBalance,
uint256 remainingBalance,
uint256 ratePerSecond
uint256 ratePerSecond
)
)
{
{
sender = streams[streamId].sender;
sender = _streams[streamId].sender;
recipient = streams[streamId].recipient;
recipient = _streams[streamId].recipient;
deposit = streams[streamId].deposit;
deposit = _streams[streamId].deposit;
tokenAddress = streams[streamId].tokenAddress;
tokenAddress = _streams[streamId].tokenAddress;
startTime = streams[streamId].startTime;
startTime = _streams[streamId].startTime;
stopTime = streams[streamId].stopTime;
stopTime = _streams[streamId].stopTime;
remainingBalance = streams[streamId].remainingBalance;
remainingBalance = _streams[streamId].remainingBalance;
ratePerSecond = streams[streamId].ratePerSecond;
ratePerSecond = _streams[streamId].ratePerSecond;
}
}
/**
/**
* @notice Returns either the delta in seconds between `block.timestamp` and `startTime` or
* @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
* between `stopTime` and `startTime, whichever is smaller. If `block.timestamp` is before
* `startTime`, it returns 0.
* `startTime`, it returns 0.
* @dev Throws if the id does not point to a valid stream.
* @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.
* @param streamId The id of the stream for which to query the delta.
* @return The time delta in seconds.
* @notice Returns the time delta in seconds.
*/
*/
function deltaOf(uint256 streamId) public view streamExists(streamId) returns (uint256 delta) {
function deltaOf(uint256 streamId)
Types.Stream memory stream = streams[streamId];
public
view
streamExists(streamId)
returns (uint256 delta)
{
Stream memory stream = _streams[streamId];
if (block.timestamp <= stream.startTime) return 0;
if (block.timestamp <= stream.startTime) return 0;
if (block.timestamp < stream.stopTime) return block.timestamp - stream.startTime;
if (block.timestamp < stream.stopTime)
return block.timestamp - stream.startTime;
return stream.stopTime - stream.startTime;
return stream.stopTime - stream.startTime;
}
}
struct BalanceOfLocalVars {
struct BalanceOfLocalVars {
MathError mathErr;
uint256 recipientBalance;
uint256 recipientBalance;
uint256 withdrawalAmount;
uint256 withdrawalAmount;
uint256 senderBalance;
uint256 senderBalance;
}
}
/**
/**
* @notice Returns the available funds for the given stream id and address.
* @notice Returns the available funds for the given stream id and address.
* @dev Throws if the id does not point to a valid stream.
* @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 streamId The id of the stream for which to query the balance.
* @param who The address 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.
* @notice Returns the total funds allocated to `who` as uint256.
*/
*/
function balanceOf(uint256 streamId, address who) public view streamExists(streamId) returns (uint256 balance) {
function balanceOf(uint256 streamId, address who)
Types.Stream memory stream = streams[streamId];
public
view
streamExists(streamId)
returns (uint256 balance)
{
Stream memory stream = _streams[streamId];
BalanceOfLocalVars memory vars;
BalanceOfLocalVars memory vars;
uint256 delta = deltaOf(streamId);
uint256 delta = deltaOf(streamId);
(vars.mathErr, vars.recipientBalance) = mulUInt(delta, stream.ratePerSecond);
vars.recipientBalance = 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.
* 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
* We have to subtract the total amount withdrawn from the amount of money that has been
* streamed until now.
* streamed until now.
*/
*/
if (stream.deposit > stream.remainingBalance) {
if (stream.deposit > stream.remainingBalance) {
(vars.mathErr, vars.withdrawalAmount) = subUInt(stream.deposit, stream.remainingBalance);
vars.withdrawalAmount = stream.deposit - stream.remainingBalance;
assert(vars.mathErr == MathError.NO_ERROR);
vars.recipientBalance =
(vars.mathErr, vars.recipientBalance) = subUInt(vars.recipientBalance, vars.withdrawalAmount);
vars.recipientBalance -
/* `withdrawalAmount` cannot and should not be bigger than `recipientBalance`. */
vars.withdrawalAmount;
assert(vars.mathErr == MathError.NO_ERROR);
}
}
if (who == stream.recipient) return vars.recipientBalance;
if (who == stream.recipient) return vars.recipientBalance;
if (who == stream.sender) {
if (who == stream.sender) {
(vars.mathErr, vars.senderBalance) = subUInt(stream.remainingBalance, vars.recipientBalance);
vars.senderBalance =
/* `recipientBalance` cannot and should not be bigger than `remainingBalance`. */
stream.remainingBalance -
assert(vars.mathErr == MathError.NO_ERROR);
vars.recipientBalance;
return vars.senderBalance;
return vars.senderBalance;
}
}
return 0;
return 0;
}
}
/*** Public Effects & Interactions Functions ***/
/*** Public Effects & Interactions Functions ***/
struct CreateStreamLocalVars {
struct CreateStreamLocalVars {
MathError mathErr;
uint256 duration;
uint256 duration;
uint256 ratePerSecond;
uint256 ratePerSecond;
}
}
/**
/**
* @notice Creates a new stream funded by `msg.sender` and paid towards `recipient`.
* @notice Creates a new stream funded by this contracts itself and paid towards `recipient`.
* @dev Throws if the recipient is the zero address, the contract itself or the caller.
* @dev Throws if the recipient is the zero address, the contract itself or the caller.
* Throws if the deposit is 0.
* Throws if the deposit is 0.
* Throws if the start time is before `block.timestamp`.
* Throws if the start time is before `block.timestamp`.
* Throws if the stop time is before the start time.
* Throws if the stop time is before the start time.
* Throws if the duration calculation has a math error.
* Throws if the duration calculation has a math error.
* Throws if the deposit is smaller than the duration.
* Throws if the deposit is smaller than the duration.
* Throws if the deposit is not a multiple of the duration.
* Throws if the deposit is not a multiple of the duration.
* Throws if the rate calculation has a math error.
* Throws if the rate calculation has a math error.
* Throws if the next stream id calculation has a math error.
* Throws if the next stream id calculation has a math error.
* Throws if the contract is not allowed to transfer enough tokens.
* Throws if the contract is not allowed to transfer enough tokens.
* Throws if there is a token transfer failure.
* Throws if there is a token transfer failure.
* @param recipient The address towards which the money is streamed.
* @param recipient The address towards which the money is streamed.
* @param deposit The amount of money to be streamed.
* @param deposit The amount of money to be streamed.
* @param tokenAddress The ERC20 token to use as streaming currency.
* @param tokenAddress The ERC20 token to use as streaming currency.
* @param startTime The unix timestamp for when the stream starts.
* @param startTime The unix timestamp for when the stream starts.
* @param stopTime The unix timestamp for when the stream stops.
* @param stopTime The unix timestamp for when the stream stops.
* @return The uint256 id of the newly created stream.
* @notice Returns the uint256 id of the newly created stream.
*/
*/
function createStream(address recipient, uint256 deposit, address tokenAddress, uint256 startTime, uint256 stopTime)
function createStream(
public
address recipient,
returns (uint256)
uint256 deposit,
{
address tokenAddress,
require(recipient != address(0x00), "stream to the zero address");
uint256 startTime,
uint256 stopTime
) external onlyFundsAdmin returns (uint256) {
require(recipient != address(0), "stream to the zero address");
require(recipient != address(this), "stream to the contract itself");
require(recipient != address(this), "stream to the contract itself");
require(recipient != msg.sender, "stream to the caller");
require(recipient != msg.sender, "stream to the caller");
require(deposit > 0, "deposit is zero");
require(deposit > 0, "deposit is zero");
require(startTime >= block.timestamp, "start time before block.timestamp");
require(
startTime >= block.timestamp,
"start time before block.timestamp"
);
require(stopTime > startTime, "stop time before the start time");
require(stopTime > startTime, "stop time before the start time");
CreateStreamLocalVars memory vars;
CreateStreamLocalVars memory vars;
(vars.mathErr, vars.duration) = subUInt(stopTime, startTime);
vars.duration = stopTime - startTime;
/* `subUInt` can only return MathError.INTEGER_UNDERFLOW but we know `stopTime` is higher than `startTime`. */
assert(vars.mathErr == MathError.NO_ERROR);
/* Without this, the rate per second would be zero. */
/* Without this, the rate per second would be zero. */
require(deposit >= vars.duration, "deposit smaller than time delta");
require(deposit >= vars.duration, "deposit smaller than time delta");
/* This condition avoids dealing with remainders */
/* This condition avoids dealing with remainders */
require(deposit % vars.duration == 0, "deposit not multiple of time delta");
require(
deposit % vars.duration == 0,
"deposit not multiple of time delta"
);
(vars.mathErr, vars.ratePerSecond) = divUInt(deposit, vars.duration);
vars.ratePerSecond = deposit / vars.duration;
/* `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. */
/* Create and store the stream object. */
uint256 streamId = nextStreamId;
uint256 streamId = _nextStreamId;
streams[streamId] = Types.Stream({
_streams[streamId] = Stream({
remainingBalance: deposit,
remainingBalance: deposit,
deposit: deposit,
deposit: deposit,
isEntity: true,
isEntity: true,
ratePerSecond: vars.ratePerSecond,
ratePerSecond: vars.ratePerSecond,
recipient: recipient,
recipient: recipient,
sender: msg.sender,
sender: address(this),
startTime: startTime,
startTime: startTime,
stopTime: stopTime,
stopTime: stopTime,
tokenAddress: tokenAddress
tokenAddress: tokenAddress
});
});
/* Increment the next stream id. */
/* Increment the next stream id. */
(vars.mathErr, nextStreamId) = addUInt(nextStreamId, uint256(1));
_nextStreamId++;
require(vars.mathErr == MathError.NO_ERROR, "next stream id calculation error");
IERC20(tokenAddress).safeTransferFrom(msg.sender, address(this), deposit);
emit CreateStream(
emit CreateStream(streamId, msg.sender, recipient, deposit, tokenAddress, startTime, stopTime);
streamId,
address(this),
recipient,
deposit,
tokenAddress,
startTime,
stopTime
);
return streamId;
return streamId;
}
}
/**
/**
* @notice Withdraws from the contract to the recipient's account.
* @notice Withdraws from the contract to the recipient's account.
* @dev Throws if the id does not point to a valid stream.
* @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 caller is not the funds admin or the recipient of the stream.
* Throws if the amount exceeds the available balance.
* Throws if the amount exceeds the available balance.
* Throws if there is a token transfer failure.
* Throws if there is a token transfer failure.
* @param streamId The id of the stream to withdraw tokens from.
* @param streamId The id of the stream to withdraw tokens from.
* @param amount The amount of tokens to withdraw.
* @param amount The amount of tokens to withdraw.
*/
*/
function withdrawFromStream(uint256 streamId, uint256 amount)
function withdrawFromStream(uint256 streamId, uint256 amount)
external
external
nonReentrant
nonReentrant
streamExists(streamId)
streamExists(streamId)
onlySenderOrRecipient(streamId)
onlyAdminOrRecipient(streamId)
returns (bool)
returns (bool)
{
{
require(amount > 0, "amount is zero");
require(amount > 0, "amount is zero");
Types.Stream memory stream = streams[streamId];
Stream memory stream = _streams[streamId];
uint256 balance = balanceOf(streamId, stream.recipient);
uint256 balance = balanceOf(streamId, stream.recipient);
require(balance >= amount, "amount exceeds the available balance");
require(balance >= amount, "amount exceeds the available balance");
MathError mathErr;
_streams[streamId].remainingBalance = stream.remainingBalance - amount;
(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];
if (_streams[streamId].remainingBalance == 0) delete _streams[streamId];
IERC20(stream.tokenAddress).safeTransfer(stream.recipient, amount);
IERC20(stream.tokenAddress).safeTransfer(stream.recipient, amount);
emit WithdrawFromStream(streamId, stream.recipient, amount);
emit WithdrawFromStream(streamId, stream.recipient, amount);
return true;
return true;
}
}
/**
/**
* @notice Cancels the stream and transfers the tokens back on a pro rata basis.
* @notice Cancels the stream and transfers the tokens back on a pro rata basis.
* @dev Throws if the id does not point to a valid stream.
* @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 caller is not the funds admin or the recipient of the stream.
* Throws if there is a token transfer failure.
* Throws if there is a token transfer failure.
* @param streamId The id of the stream to cancel.
* @param streamId The id of the stream to cancel.
* @return bool true=success, otherwise false.
* @notice Returns bool true=success, otherwise false.
*/
*/
function cancelStream(uint256 streamId)
function cancelStream(uint256 streamId)
external
external
nonReentrant
nonReentrant
streamExists(streamId)
streamExists(streamId)
onlySenderOrRecipient(streamId)
onlyAdminOrRecipient(streamId)
returns (bool)
returns (bool)
{
{
Types.Stream memory stream = streams[streamId];
Stream memory stream = _streams[streamId];
uint256 senderBalance = balanceOf(streamId, stream.sender);
uint256 senderBalance = balanceOf(streamId, stream.sender);
uint256 recipientBalance = balanceOf(streamId, stream.recipient);
uint256 recipientBalance = balanceOf(streamId, stream.recipient);
delete streams[streamId];
delete _streams[streamId];
IERC20 token = IERC20(stream.tokenAddress);
IERC20 token = IERC20(stream.tokenAddress);
if (recipientBalance > 0) token.safeTransfer(stream.recipient, recipientBalance);
if (recipientBalance > 0)
if (senderBalance > 0) token.safeTransfer(stream.sender, senderBalance);
token.safeTransfer(stream.recipient, recipientBalance);
emit CancelStream(streamId, stream.sender, stream.recipient, senderBalance, recipientBalance);
emit CancelStream(
streamId,
stream.sender,
stream.recipient,
senderBalance,
recipientBalance
);
return true;
return true;
}
}
}
}