Skip to content
This repository was archived by the owner on Oct 21, 2025. It is now read-only.

Commit d1eab91

Browse files
authored
Merge pull request #91 from aave/feat/81-add-ecosystem-reserve-contracts
Add contracts for reserve ecosystem updates
2 parents 448f864 + 5d7574e commit d1eab91

12 files changed

+1105
-5
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,23 @@ Aave is a decentralized non-custodial liquidity markets protocol where users can
2222
## Connect with the community
2323

2424
You can join at the [Discord](http://aave.com/discord) channel or at the [Governance Forum](https://governance.aave.com/) for asking questions about the protocol or talk about Aave with other peers.
25+
26+
## Getting started
27+
28+
Download the dependencies
29+
30+
```
31+
npm i
32+
```
33+
34+
Compile the contracts
35+
36+
```
37+
npm run compile
38+
```
39+
40+
## Running tests
41+
42+
```
43+
npm test
44+
```
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.10;
3+
4+
import {Ownable} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/Ownable.sol';
5+
import {IStreamable} from './interfaces/IStreamable.sol';
6+
import {IAdminControlledEcosystemReserve} from './interfaces/IAdminControlledEcosystemReserve.sol';
7+
import {IAaveEcosystemReserveController} from './interfaces/IAaveEcosystemReserveController.sol';
8+
import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol';
9+
10+
contract AaveEcosystemReserveController is Ownable, IAaveEcosystemReserveController {
11+
/**
12+
* @notice Constructor.
13+
* @param aaveGovShortTimelock The address of the Aave's governance executor, owning this contract
14+
*/
15+
constructor(address aaveGovShortTimelock) {
16+
transferOwnership(aaveGovShortTimelock);
17+
}
18+
19+
/// @inheritdoc IAaveEcosystemReserveController
20+
function approve(
21+
address collector,
22+
IERC20 token,
23+
address recipient,
24+
uint256 amount
25+
) external onlyOwner {
26+
IAdminControlledEcosystemReserve(collector).approve(token, recipient, amount);
27+
}
28+
29+
/// @inheritdoc IAaveEcosystemReserveController
30+
function transfer(
31+
address collector,
32+
IERC20 token,
33+
address recipient,
34+
uint256 amount
35+
) external onlyOwner {
36+
IAdminControlledEcosystemReserve(collector).transfer(token, recipient, amount);
37+
}
38+
39+
/// @inheritdoc IAaveEcosystemReserveController
40+
function createStream(
41+
address collector,
42+
address recipient,
43+
uint256 deposit,
44+
IERC20 tokenAddress,
45+
uint256 startTime,
46+
uint256 stopTime
47+
) external onlyOwner returns (uint256) {
48+
return
49+
IStreamable(collector).createStream(
50+
recipient,
51+
deposit,
52+
address(tokenAddress),
53+
startTime,
54+
stopTime
55+
);
56+
}
57+
58+
/// @inheritdoc IAaveEcosystemReserveController
59+
function withdrawFromStream(
60+
address collector,
61+
uint256 streamId,
62+
uint256 funds
63+
) external onlyOwner returns (bool) {
64+
return IStreamable(collector).withdrawFromStream(streamId, funds);
65+
}
66+
67+
/// @inheritdoc IAaveEcosystemReserveController
68+
function cancelStream(address collector, uint256 streamId) external onlyOwner returns (bool) {
69+
return IStreamable(collector).cancelStream(streamId);
70+
}
71+
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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

Comments
 (0)