Skip to content

Commit d203d4c

Browse files
authored
feat: Collector revision-6 Upgrade. (#82)
### Context The Collector contract (Revision 5) has the limitation of only allowing one address to call its functions, the `_fundsAdmin`, which currently is the Governance Executor contract. To enable the possibility of the [FinanceSteward](https://governance.aave.com/t/arfc-aave-finance-steward/17570) contract to be allowed to call the Collector alongside the Executor, it is recommended to use the `ACL_MANAGER` to manage its access control. A new role is going to be created named `FUNDS_ADMIN` ### Changelog Collector * Initialize function changes to only populate the `nextStreamId` * Created constructor to setup the `ACL_MANAGER` contract address as immutable variable * deprecated `_fundsAdmin` variable * removed getter and setter functions for `_fundsAdmin` and relevant errors and events. * introduced new function to check if an address has the `FUNDS_ADMIN` role named `IsFundsAdmin` * New tests for the Collector
1 parent be37b99 commit d203d4c

File tree

6 files changed

+564
-73
lines changed

6 files changed

+564
-73
lines changed

src/contracts/treasury/Collector.sol

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.0;
33

44
import {ICollector} from './ICollector.sol';
5+
import {IAccessControl} from '../dependencies/openzeppelin/contracts/IAccessControl.sol';
56
import {ReentrancyGuard} from '../dependencies/openzeppelin/ReentrancyGuard.sol';
67
import {VersionedInitializable} from '../misc/aave-upgradeability/VersionedInitializable.sol';
78
import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol';
@@ -28,14 +29,23 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
2829
/*** Storage Properties ***/
2930

3031
/**
31-
* @notice Address of the current funds admin.
32+
* @notice Current revision of the contract.
3233
*/
33-
address internal _fundsAdmin;
34+
uint256 public constant REVISION = 6;
35+
36+
/// @inheritdoc ICollector
37+
address public constant ETH_MOCK_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
38+
39+
/// @inheritdoc ICollector
40+
bytes32 public constant FUNDS_ADMIN_ROLE = 'FUNDS_ADMIN';
41+
42+
/// @inheritdoc ICollector
43+
address public immutable ACL_MANAGER;
3444

3545
/**
36-
* @notice Current revision of the contract.
46+
* @notice [DEPRECATED] Use `isFundsAdmin()` to check address.
3747
*/
38-
uint256 public constant REVISION = 5;
48+
address internal _fundsAdmin_deprecated;
3949

4050
/**
4151
* @notice Counter for new stream ids.
@@ -47,16 +57,15 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
4757
*/
4858
mapping(uint256 => Stream) private _streams;
4959

50-
/// @inheritdoc ICollector
51-
address public constant ETH_MOCK_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
52-
5360
/*** Modifiers ***/
5461

5562
/**
56-
* @dev Throws if the caller is not the funds admin.
63+
* @dev Throws if the caller does not have the FUNDS_ADMIN role
5764
*/
5865
modifier onlyFundsAdmin() {
59-
require(msg.sender == _fundsAdmin, 'ONLY_BY_FUNDS_ADMIN');
66+
if (_onlyFundsAdmin() == false) {
67+
revert OnlyFundsAdmin();
68+
}
6069
_;
6170
}
6271

@@ -65,30 +74,32 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
6574
* @param streamId The id of the stream to query.
6675
*/
6776
modifier onlyAdminOrRecipient(uint256 streamId) {
68-
require(
69-
msg.sender == _fundsAdmin || msg.sender == _streams[streamId].recipient,
70-
'caller is not the funds admin or the recipient of the stream'
71-
);
77+
if (_onlyFundsAdmin() == false && msg.sender != _streams[streamId].recipient) {
78+
revert OnlyFundsAdminOrRceipient();
79+
}
7280
_;
7381
}
7482

7583
/**
7684
* @dev Throws if the provided id does not point to a valid stream.
7785
*/
7886
modifier streamExists(uint256 streamId) {
79-
require(_streams[streamId].isEntity, 'stream does not exist');
87+
if (!_streams[streamId].isEntity) revert StreamDoesNotExist();
8088
_;
8189
}
8290

91+
constructor(address aclManager) {
92+
if (aclManager == address(0)) revert InvalidZeroAddress();
93+
ACL_MANAGER = aclManager;
94+
}
95+
8396
/*** Contract Logic Starts Here */
8497

8598
/// @inheritdoc ICollector
86-
function initialize(address fundsAdmin, uint256 nextStreamId) external initializer {
99+
function initialize(uint256 nextStreamId) external virtual initializer {
87100
if (nextStreamId != 0) {
88101
_nextStreamId = nextStreamId;
89102
}
90-
91-
_setFundsAdmin(fundsAdmin);
92103
}
93104

94105
/*** View Functions ***/
@@ -99,8 +110,8 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
99110
}
100111

101112
/// @inheritdoc ICollector
102-
function getFundsAdmin() external view returns (address) {
103-
return _fundsAdmin;
113+
function isFundsAdmin(address admin) external view returns (bool) {
114+
return IAccessControl(ACL_MANAGER).hasRole(FUNDS_ADMIN_ROLE, admin);
104115
}
105116

106117
/// @inheritdoc ICollector
@@ -195,7 +206,7 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
195206

196207
/// @inheritdoc ICollector
197208
function transfer(IERC20 token, address recipient, uint256 amount) external onlyFundsAdmin {
198-
require(recipient != address(0), 'INVALID_0X_RECIPIENT');
209+
if (recipient == address(0)) revert InvalidZeroAddress();
199210

200211
if (address(token) == ETH_MOCK_ADDRESS) {
201212
payable(recipient).sendValue(amount);
@@ -204,18 +215,8 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
204215
}
205216
}
206217

207-
/// @inheritdoc ICollector
208-
function setFundsAdmin(address admin) external onlyFundsAdmin {
209-
_setFundsAdmin(admin);
210-
}
211-
212-
/**
213-
* @dev Transfer the ownership of the funds administrator role.
214-
* @param admin The address of the new funds administrator
215-
*/
216-
function _setFundsAdmin(address admin) internal {
217-
_fundsAdmin = admin;
218-
emit NewFundsAdmin(admin);
218+
function _onlyFundsAdmin() internal view returns (bool) {
219+
return IAccessControl(ACL_MANAGER).hasRole(FUNDS_ADMIN_ROLE, msg.sender);
219220
}
220221

221222
struct CreateStreamLocalVars {
@@ -244,21 +245,21 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
244245
uint256 startTime,
245246
uint256 stopTime
246247
) external onlyFundsAdmin returns (uint256) {
247-
require(recipient != address(0), 'stream to the zero address');
248-
require(recipient != address(this), 'stream to the contract itself');
249-
require(recipient != msg.sender, 'stream to the caller');
250-
require(deposit > 0, 'deposit is zero');
251-
require(startTime >= block.timestamp, 'start time before block.timestamp');
252-
require(stopTime > startTime, 'stop time before the start time');
248+
if (recipient == address(0)) revert InvalidZeroAddress();
249+
if (recipient == address(this)) revert InvalidRecipient();
250+
if (recipient == msg.sender) revert InvalidRecipient();
251+
if (deposit == 0) revert InvalidZeroAmount();
252+
if (startTime < block.timestamp) revert InvalidStartTime();
253+
if (stopTime <= startTime) revert InvalidStopTime();
253254

254255
CreateStreamLocalVars memory vars;
255256
vars.duration = stopTime - startTime;
256257

257258
/* Without this, the rate per second would be zero. */
258-
require(deposit >= vars.duration, 'deposit smaller than time delta');
259+
if (deposit < vars.duration) revert DepositSmallerTimeDelta();
259260

260261
/* This condition avoids dealing with remainders */
261-
require(deposit % vars.duration == 0, 'deposit not multiple of time delta');
262+
if (deposit % vars.duration > 0) revert DepositNotMultipleTimeDelta();
262263

263264
vars.ratePerSecond = deposit / vars.duration;
264265

@@ -302,11 +303,11 @@ contract Collector is VersionedInitializable, ICollector, ReentrancyGuard {
302303
uint256 streamId,
303304
uint256 amount
304305
) external nonReentrant streamExists(streamId) onlyAdminOrRecipient(streamId) returns (bool) {
305-
require(amount > 0, 'amount is zero');
306+
if (amount == 0) revert InvalidZeroAmount();
306307
Stream memory stream = _streams[streamId];
307308

308309
uint256 balance = balanceOf(streamId, stream.recipient);
309-
require(balance >= amount, 'amount exceeds the available balance');
310+
if (balance < amount) revert BalanceExceeded();
310311

311312
_streams[streamId].remainingBalance = stream.remainingBalance - amount;
312313

src/contracts/treasury/ICollector.sol

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,60 @@ interface ICollector {
1616
bool isEntity;
1717
}
1818

19-
/** @notice Emitted when the funds admin changes
20-
* @param fundsAdmin The new funds admin.
21-
**/
22-
event NewFundsAdmin(address indexed fundsAdmin);
19+
/**
20+
* @dev Withdraw amount exceeds available balance
21+
*/
22+
error BalanceExceeded();
23+
24+
/**
25+
* @dev Deposit smaller than time delta
26+
*/
27+
error DepositSmallerTimeDelta();
28+
29+
/**
30+
* @dev Deposit not multiple of time delta
31+
*/
32+
error DepositNotMultipleTimeDelta();
33+
34+
/**
35+
* @dev Recipient cannot be the contract itself or msg.sender
36+
*/
37+
error InvalidRecipient();
38+
39+
/**
40+
* @dev Start time cannot be before block.timestamp
41+
*/
42+
error InvalidStartTime();
43+
44+
/**
45+
* @dev Stop time must be greater than startTime
46+
*/
47+
error InvalidStopTime();
48+
49+
/**
50+
* @dev Provided address cannot be the zero-address
51+
*/
52+
error InvalidZeroAddress();
53+
54+
/**
55+
* @dev Amount cannot be zero
56+
*/
57+
error InvalidZeroAmount();
58+
59+
/**
60+
* @dev Only caller with FUNDS_ADMIN role can call
61+
*/
62+
error OnlyFundsAdmin();
63+
64+
/**
65+
* @dev Only caller with FUNDS_ADMIN role or stream recipient can call
66+
*/
67+
error OnlyFundsAdminOrRceipient();
68+
69+
/**
70+
* @dev The provided ID does not belong to an existing stream
71+
*/
72+
error StreamDoesNotExist();
2373

2474
/** @notice Emitted when the new stream is created
2575
* @param streamId The identifier of the stream.
@@ -64,29 +114,38 @@ interface ICollector {
64114
uint256 recipientBalance
65115
);
66116

117+
/**
118+
* @notice FUNDS_ADMIN role granted by ACL Manager
119+
**/
120+
function FUNDS_ADMIN_ROLE() external view returns (bytes32);
121+
122+
/**
123+
* @notice Address of the current ACL Manager.
124+
**/
125+
function ACL_MANAGER() external view returns (address);
126+
67127
/** @notice Returns the mock ETH reference address
68128
* @return address The address
69129
**/
70130
function ETH_MOCK_ADDRESS() external pure returns (address);
71131

72132
/** @notice Initializes the contracts
73-
* @param fundsAdmin Funds admin address
74133
* @param nextStreamId StreamId to set, applied if greater than 0
75134
**/
76-
function initialize(address fundsAdmin, uint256 nextStreamId) external;
135+
function initialize(uint256 nextStreamId) external;
77136

78137
/**
79-
* @notice Return the funds admin, only entity to be able to interact with this contract (controller of reserve)
80-
* @return address The address of the funds admin
138+
* @notice Checks if address is funds admin
139+
* @return bool If the address has the funds admin role
81140
**/
82-
function getFundsAdmin() external view returns (address);
141+
function isFundsAdmin(address admin) external view returns (bool);
83142

84143
/**
85144
* @notice Returns the available funds for the given stream id and address.
86145
* @param streamId The id of the stream for which to query the balance.
87146
* @param who The address for which to query the balance.
88147
* @notice Returns the total funds allocated to `who` as uint256.
89-
*/
148+
**/
90149
function balanceOf(uint256 streamId, address who) external view returns (uint256 balance);
91150

92151
/**
@@ -105,13 +164,6 @@ interface ICollector {
105164
**/
106165
function transfer(IERC20 token, address recipient, uint256 amount) external;
107166

108-
/**
109-
* @dev Transfer the ownership of the funds administrator role.
110-
This function should only be callable by the current funds administrator.
111-
* @param admin The address of the new funds administrator
112-
*/
113-
function setFundsAdmin(address admin) external;
114-
115167
/**
116168
* @notice Creates a new stream funded by this contracts itself and paid towards `recipient`.
117169
* @param recipient The address towards which the money is streamed.

src/deployments/contracts/procedures/AaveV3TreasuryProcedure.sol

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,42 +15,35 @@ contract AaveV3TreasuryProcedure {
1515
function _deployAaveV3Treasury(
1616
address poolAdmin,
1717
address deployedProxyAdmin,
18+
address aclManager,
1819
bytes32 collectorSalt
1920
) internal returns (TreasuryReport memory) {
2021
TreasuryReport memory treasuryReport;
2122
bytes32 salt = collectorSalt;
2223
address treasuryOwner = poolAdmin;
2324

2425
if (salt != '') {
25-
Collector treasuryImplementation = new Collector{salt: salt}();
26-
treasuryImplementation.initialize(address(0), 0);
26+
Collector treasuryImplementation = new Collector{salt: salt}(aclManager);
27+
treasuryImplementation.initialize(0);
2728
treasuryReport.treasuryImplementation = address(treasuryImplementation);
2829

2930
treasuryReport.treasury = address(
3031
new TransparentUpgradeableProxy{salt: salt}(
3132
treasuryReport.treasuryImplementation,
3233
ProxyAdmin(deployedProxyAdmin),
33-
abi.encodeWithSelector(
34-
treasuryImplementation.initialize.selector,
35-
address(treasuryOwner),
36-
0
37-
)
34+
abi.encodeWithSelector(treasuryImplementation.initialize.selector, 100_000)
3835
)
3936
);
4037
} else {
41-
Collector treasuryImplementation = new Collector();
42-
treasuryImplementation.initialize(address(0), 0);
38+
Collector treasuryImplementation = new Collector(aclManager);
39+
treasuryImplementation.initialize(0);
4340
treasuryReport.treasuryImplementation = address(treasuryImplementation);
4441

4542
treasuryReport.treasury = address(
4643
new TransparentUpgradeableProxy(
4744
treasuryReport.treasuryImplementation,
4845
ProxyAdmin(deployedProxyAdmin),
49-
abi.encodeWithSelector(
50-
treasuryImplementation.initialize.selector,
51-
address(treasuryOwner),
52-
100_000
53-
)
46+
abi.encodeWithSelector(treasuryImplementation.initialize.selector, 100_000)
5447
)
5548
);
5649
}

src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ contract AaveV3PeripheryBatch is
3131

3232
_report.aaveOracle = _deployAaveOracle(config.oracleDecimals, poolAddressesProvider);
3333

34+
address aclManager = address(1); // temp-to-run-tests
35+
3436
if (config.treasury == address(0)) {
3537
TreasuryReport memory treasuryReport = _deployAaveV3Treasury(
3638
poolAdmin,
3739
_report.proxyAdmin,
40+
aclManager,
3841
config.salt
3942
);
4043

tests/deployments/AaveV3PermissionsTest.t.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ contract AaveV3PermissionsTest is BatchTestProcedures {
305305
report.proxyAdmin,
306306
'Treasury proxy admin does not match with report.proxyAdmin'
307307
);
308+
assertEq(ICollector(report.treasury).ACL_MANAGER(), report.aclManager);
308309
}
309310
{
310311
address proxyAdminOwner = Ownable(report.proxyAdmin).owner();

0 commit comments

Comments
 (0)