|
| 1 | +// SPDX-License-Identifier: GPL-3.0 |
| 2 | +pragma solidity ^0.8.10; |
| 3 | + |
| 4 | +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; |
| 5 | +import {IStreamable} from './interfaces/IStreamable.sol'; |
| 6 | +import {AdminControlledEcosystemReserve} from './AdminControlledEcosystemReserve.sol'; |
| 7 | +import {ReentrancyGuard} from './libs/ReentrancyGuard.sol'; |
| 8 | +import {SafeERC20} from './libs/SafeERC20.sol'; |
| 9 | + |
| 10 | +/** |
| 11 | + * @title AaveEcosystemReserve v2 |
| 12 | + * @notice Stores ERC20 tokens of an ecosystem reserve, adding streaming capabilities. |
| 13 | + * Modification of Sablier https://github.com/sablierhq/sablier/blob/develop/packages/protocol/contracts/Sablier.sol |
| 14 | + * Original can be found also deployed on https://etherscan.io/address/0xCD18eAa163733Da39c232722cBC4E8940b1D8888 |
| 15 | + * Modifications: |
| 16 | + * - Sablier "pulls" the funds from the creator of the stream at creation. In the Aave case, we already have the funds. |
| 17 | + * - Anybody can create streams on Sablier. Here, only the funds admin (Aave governance via controller) can |
| 18 | + * - Adapted codebase to Solidity 0.8.11, mainly removing SafeMath and CarefulMath to use native safe math |
| 19 | + * - Same as with creation, on Sablier the `sender` and `recipient` can cancel a stream. Here, only fund admin and recipient |
| 20 | + * @author BGD Labs |
| 21 | + **/ |
| 22 | +contract AaveEcosystemReserveV2 is AdminControlledEcosystemReserve, ReentrancyGuard, IStreamable { |
| 23 | + using SafeERC20 for IERC20; |
| 24 | + |
| 25 | + /*** Storage Properties ***/ |
| 26 | + |
| 27 | + /** |
| 28 | + * @notice Counter for new stream ids. |
| 29 | + */ |
| 30 | + uint256 private _nextStreamId; |
| 31 | + |
| 32 | + /** |
| 33 | + * @notice The stream objects identifiable by their unsigned integer ids. |
| 34 | + */ |
| 35 | + mapping(uint256 => Stream) private _streams; |
| 36 | + |
| 37 | + /*** Modifiers ***/ |
| 38 | + |
| 39 | + /** |
| 40 | + * @dev Throws if the caller is not the funds admin of the recipient of the stream. |
| 41 | + */ |
| 42 | + modifier onlyAdminOrRecipient(uint256 streamId) { |
| 43 | + require( |
| 44 | + msg.sender == _fundsAdmin || msg.sender == _streams[streamId].recipient, |
| 45 | + 'caller is not the funds admin or the recipient of the stream' |
| 46 | + ); |
| 47 | + _; |
| 48 | + } |
| 49 | + |
| 50 | + /** |
| 51 | + * @dev Throws if the provided id does not point to a valid stream. |
| 52 | + */ |
| 53 | + modifier streamExists(uint256 streamId) { |
| 54 | + require(_streams[streamId].isEntity, 'stream does not exist'); |
| 55 | + _; |
| 56 | + } |
| 57 | + |
| 58 | + /*** Contract Logic Starts Here */ |
| 59 | + |
| 60 | + function initialize(address fundsAdmin) external initializer { |
| 61 | + _nextStreamId = 100000; |
| 62 | + _setFundsAdmin(fundsAdmin); |
| 63 | + } |
| 64 | + |
| 65 | + /*** View Functions ***/ |
| 66 | + |
| 67 | + /** |
| 68 | + * @notice Returns the next available stream id |
| 69 | + * @notice Returns the stream id. |
| 70 | + */ |
| 71 | + function getNextStreamId() external view returns (uint256) { |
| 72 | + return _nextStreamId; |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * @notice Returns the stream with all its properties. |
| 77 | + * @dev Throws if the id does not point to a valid stream. |
| 78 | + * @param streamId The id of the stream to query. |
| 79 | + * @notice Returns the stream object. |
| 80 | + */ |
| 81 | + function getStream(uint256 streamId) |
| 82 | + external |
| 83 | + view |
| 84 | + streamExists(streamId) |
| 85 | + returns ( |
| 86 | + address sender, |
| 87 | + address recipient, |
| 88 | + uint256 deposit, |
| 89 | + address tokenAddress, |
| 90 | + uint256 startTime, |
| 91 | + uint256 stopTime, |
| 92 | + uint256 remainingBalance, |
| 93 | + uint256 ratePerSecond |
| 94 | + ) |
| 95 | + { |
| 96 | + sender = _streams[streamId].sender; |
| 97 | + recipient = _streams[streamId].recipient; |
| 98 | + deposit = _streams[streamId].deposit; |
| 99 | + tokenAddress = _streams[streamId].tokenAddress; |
| 100 | + startTime = _streams[streamId].startTime; |
| 101 | + stopTime = _streams[streamId].stopTime; |
| 102 | + remainingBalance = _streams[streamId].remainingBalance; |
| 103 | + ratePerSecond = _streams[streamId].ratePerSecond; |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * @notice Returns either the delta in seconds between `block.timestamp` and `startTime` or |
| 108 | + * between `stopTime` and `startTime, whichever is smaller. If `block.timestamp` is before |
| 109 | + * `startTime`, it returns 0. |
| 110 | + * @dev Throws if the id does not point to a valid stream. |
| 111 | + * @param streamId The id of the stream for which to query the delta. |
| 112 | + * @notice Returns the time delta in seconds. |
| 113 | + */ |
| 114 | + function deltaOf(uint256 streamId) public view streamExists(streamId) returns (uint256 delta) { |
| 115 | + Stream memory stream = _streams[streamId]; |
| 116 | + if (block.timestamp <= stream.startTime) return 0; |
| 117 | + if (block.timestamp < stream.stopTime) return block.timestamp - stream.startTime; |
| 118 | + return stream.stopTime - stream.startTime; |
| 119 | + } |
| 120 | + |
| 121 | + struct BalanceOfLocalVars { |
| 122 | + uint256 recipientBalance; |
| 123 | + uint256 withdrawalAmount; |
| 124 | + uint256 senderBalance; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * @notice Returns the available funds for the given stream id and address. |
| 129 | + * @dev Throws if the id does not point to a valid stream. |
| 130 | + * @param streamId The id of the stream for which to query the balance. |
| 131 | + * @param who The address for which to query the balance. |
| 132 | + * @notice Returns the total funds allocated to `who` as uint256. |
| 133 | + */ |
| 134 | + function balanceOf(uint256 streamId, address who) |
| 135 | + public |
| 136 | + view |
| 137 | + streamExists(streamId) |
| 138 | + returns (uint256 balance) |
| 139 | + { |
| 140 | + Stream memory stream = _streams[streamId]; |
| 141 | + BalanceOfLocalVars memory vars; |
| 142 | + |
| 143 | + uint256 delta = deltaOf(streamId); |
| 144 | + vars.recipientBalance = delta * stream.ratePerSecond; |
| 145 | + |
| 146 | + /* |
| 147 | + * If the stream `balance` does not equal `deposit`, it means there have been withdrawals. |
| 148 | + * We have to subtract the total amount withdrawn from the amount of money that has been |
| 149 | + * streamed until now. |
| 150 | + */ |
| 151 | + if (stream.deposit > stream.remainingBalance) { |
| 152 | + vars.withdrawalAmount = stream.deposit - stream.remainingBalance; |
| 153 | + vars.recipientBalance = vars.recipientBalance - vars.withdrawalAmount; |
| 154 | + } |
| 155 | + |
| 156 | + if (who == stream.recipient) return vars.recipientBalance; |
| 157 | + if (who == stream.sender) { |
| 158 | + vars.senderBalance = stream.remainingBalance - vars.recipientBalance; |
| 159 | + return vars.senderBalance; |
| 160 | + } |
| 161 | + return 0; |
| 162 | + } |
| 163 | + |
| 164 | + /*** Public Effects & Interactions Functions ***/ |
| 165 | + |
| 166 | + struct CreateStreamLocalVars { |
| 167 | + uint256 duration; |
| 168 | + uint256 ratePerSecond; |
| 169 | + } |
| 170 | + |
| 171 | + /** |
| 172 | + * @notice Creates a new stream funded by this contracts itself and paid towards `recipient`. |
| 173 | + * @dev Throws if the recipient is the zero address, the contract itself or the caller. |
| 174 | + * Throws if the deposit is 0. |
| 175 | + * Throws if the start time is before `block.timestamp`. |
| 176 | + * Throws if the stop time is before the start time. |
| 177 | + * Throws if the duration calculation has a math error. |
| 178 | + * Throws if the deposit is smaller than the duration. |
| 179 | + * Throws if the deposit is not a multiple of the duration. |
| 180 | + * Throws if the rate calculation has a math error. |
| 181 | + * Throws if the next stream id calculation has a math error. |
| 182 | + * Throws if the contract is not allowed to transfer enough tokens. |
| 183 | + * Throws if there is a token transfer failure. |
| 184 | + * @param recipient The address towards which the money is streamed. |
| 185 | + * @param deposit The amount of money to be streamed. |
| 186 | + * @param tokenAddress The ERC20 token to use as streaming currency. |
| 187 | + * @param startTime The unix timestamp for when the stream starts. |
| 188 | + * @param stopTime The unix timestamp for when the stream stops. |
| 189 | + * @notice Returns the uint256 id of the newly created stream. |
| 190 | + */ |
| 191 | + function createStream( |
| 192 | + address recipient, |
| 193 | + uint256 deposit, |
| 194 | + address tokenAddress, |
| 195 | + uint256 startTime, |
| 196 | + uint256 stopTime |
| 197 | + ) external onlyFundsAdmin returns (uint256) { |
| 198 | + require(recipient != address(0), 'stream to the zero address'); |
| 199 | + require(recipient != address(this), 'stream to the contract itself'); |
| 200 | + require(recipient != msg.sender, 'stream to the caller'); |
| 201 | + require(deposit > 0, 'deposit is zero'); |
| 202 | + require(startTime >= block.timestamp, 'start time before block.timestamp'); |
| 203 | + require(stopTime > startTime, 'stop time before the start time'); |
| 204 | + |
| 205 | + CreateStreamLocalVars memory vars; |
| 206 | + vars.duration = stopTime - startTime; |
| 207 | + |
| 208 | + /* Without this, the rate per second would be zero. */ |
| 209 | + require(deposit >= vars.duration, 'deposit smaller than time delta'); |
| 210 | + |
| 211 | + /* This condition avoids dealing with remainders */ |
| 212 | + require(deposit % vars.duration == 0, 'deposit not multiple of time delta'); |
| 213 | + |
| 214 | + vars.ratePerSecond = deposit / vars.duration; |
| 215 | + |
| 216 | + /* Create and store the stream object. */ |
| 217 | + uint256 streamId = _nextStreamId; |
| 218 | + _streams[streamId] = Stream({ |
| 219 | + remainingBalance: deposit, |
| 220 | + deposit: deposit, |
| 221 | + isEntity: true, |
| 222 | + ratePerSecond: vars.ratePerSecond, |
| 223 | + recipient: recipient, |
| 224 | + sender: address(this), |
| 225 | + startTime: startTime, |
| 226 | + stopTime: stopTime, |
| 227 | + tokenAddress: tokenAddress |
| 228 | + }); |
| 229 | + |
| 230 | + /* Increment the next stream id. */ |
| 231 | + _nextStreamId++; |
| 232 | + |
| 233 | + emit CreateStream( |
| 234 | + streamId, |
| 235 | + address(this), |
| 236 | + recipient, |
| 237 | + deposit, |
| 238 | + tokenAddress, |
| 239 | + startTime, |
| 240 | + stopTime |
| 241 | + ); |
| 242 | + return streamId; |
| 243 | + } |
| 244 | + |
| 245 | + /** |
| 246 | + * @notice Withdraws from the contract to the recipient's account. |
| 247 | + * @dev Throws if the id does not point to a valid stream. |
| 248 | + * Throws if the caller is not the funds admin or the recipient of the stream. |
| 249 | + * Throws if the amount exceeds the available balance. |
| 250 | + * Throws if there is a token transfer failure. |
| 251 | + * @param streamId The id of the stream to withdraw tokens from. |
| 252 | + * @param amount The amount of tokens to withdraw. |
| 253 | + */ |
| 254 | + function withdrawFromStream(uint256 streamId, uint256 amount) |
| 255 | + external |
| 256 | + nonReentrant |
| 257 | + streamExists(streamId) |
| 258 | + onlyAdminOrRecipient(streamId) |
| 259 | + returns (bool) |
| 260 | + { |
| 261 | + require(amount > 0, 'amount is zero'); |
| 262 | + Stream memory stream = _streams[streamId]; |
| 263 | + |
| 264 | + uint256 balance = balanceOf(streamId, stream.recipient); |
| 265 | + require(balance >= amount, 'amount exceeds the available balance'); |
| 266 | + |
| 267 | + _streams[streamId].remainingBalance = stream.remainingBalance - amount; |
| 268 | + |
| 269 | + if (_streams[streamId].remainingBalance == 0) delete _streams[streamId]; |
| 270 | + |
| 271 | + IERC20(stream.tokenAddress).safeTransfer(stream.recipient, amount); |
| 272 | + emit WithdrawFromStream(streamId, stream.recipient, amount); |
| 273 | + return true; |
| 274 | + } |
| 275 | + |
| 276 | + /** |
| 277 | + * @notice Cancels the stream and transfers the tokens back on a pro rata basis. |
| 278 | + * @dev Throws if the id does not point to a valid stream. |
| 279 | + * Throws if the caller is not the funds admin or the recipient of the stream. |
| 280 | + * Throws if there is a token transfer failure. |
| 281 | + * @param streamId The id of the stream to cancel. |
| 282 | + * @notice Returns bool true=success, otherwise false. |
| 283 | + */ |
| 284 | + function cancelStream(uint256 streamId) |
| 285 | + external |
| 286 | + nonReentrant |
| 287 | + streamExists(streamId) |
| 288 | + onlyAdminOrRecipient(streamId) |
| 289 | + returns (bool) |
| 290 | + { |
| 291 | + Stream memory stream = _streams[streamId]; |
| 292 | + uint256 senderBalance = balanceOf(streamId, stream.sender); |
| 293 | + uint256 recipientBalance = balanceOf(streamId, stream.recipient); |
| 294 | + |
| 295 | + delete _streams[streamId]; |
| 296 | + |
| 297 | + IERC20 token = IERC20(stream.tokenAddress); |
| 298 | + if (recipientBalance > 0) token.safeTransfer(stream.recipient, recipientBalance); |
| 299 | + |
| 300 | + emit CancelStream(streamId, stream.sender, stream.recipient, senderBalance, recipientBalance); |
| 301 | + return true; |
| 302 | + } |
| 303 | +} |
0 commit comments