diff --git a/README.md b/README.md index 77df9e1d..383d7a36 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,23 @@ Aave is a decentralized non-custodial liquidity markets protocol where users can ## Connect with the community 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. + +## Getting started + +Download the dependencies + +``` +npm i +``` + +Compile the contracts + +``` +npm run compile +``` + +## Running tests + +``` +npm test +``` diff --git a/contracts/treasury/AaveEcosystemReserveController.sol b/contracts/treasury/AaveEcosystemReserveController.sol new file mode 100644 index 00000000..fd7152df --- /dev/null +++ b/contracts/treasury/AaveEcosystemReserveController.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Ownable} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/Ownable.sol'; +import {IStreamable} from './interfaces/IStreamable.sol'; +import {IAdminControlledEcosystemReserve} from './interfaces/IAdminControlledEcosystemReserve.sol'; +import {IAaveEcosystemReserveController} from './interfaces/IAaveEcosystemReserveController.sol'; +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; + +contract AaveEcosystemReserveController is Ownable, IAaveEcosystemReserveController { + /** + * @notice Constructor. + * @param aaveGovShortTimelock The address of the Aave's governance executor, owning this contract + */ + constructor(address aaveGovShortTimelock) { + transferOwnership(aaveGovShortTimelock); + } + + /// @inheritdoc IAaveEcosystemReserveController + function approve( + address collector, + IERC20 token, + address recipient, + uint256 amount + ) external onlyOwner { + IAdminControlledEcosystemReserve(collector).approve(token, recipient, amount); + } + + /// @inheritdoc IAaveEcosystemReserveController + function transfer( + address collector, + IERC20 token, + address recipient, + uint256 amount + ) external onlyOwner { + IAdminControlledEcosystemReserve(collector).transfer(token, recipient, amount); + } + + /// @inheritdoc IAaveEcosystemReserveController + function createStream( + address collector, + address recipient, + uint256 deposit, + IERC20 tokenAddress, + uint256 startTime, + uint256 stopTime + ) external onlyOwner returns (uint256) { + return + IStreamable(collector).createStream( + recipient, + deposit, + address(tokenAddress), + startTime, + stopTime + ); + } + + /// @inheritdoc IAaveEcosystemReserveController + function withdrawFromStream( + address collector, + uint256 streamId, + uint256 funds + ) external onlyOwner returns (bool) { + return IStreamable(collector).withdrawFromStream(streamId, funds); + } + + /// @inheritdoc IAaveEcosystemReserveController + function cancelStream(address collector, uint256 streamId) external onlyOwner returns (bool) { + return IStreamable(collector).cancelStream(streamId); + } +} diff --git a/contracts/treasury/AaveEcosystemReserveV2.sol b/contracts/treasury/AaveEcosystemReserveV2.sol new file mode 100644 index 00000000..0c36f677 --- /dev/null +++ b/contracts/treasury/AaveEcosystemReserveV2.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.10; + +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {IStreamable} from './interfaces/IStreamable.sol'; +import {AdminControlledEcosystemReserve} from './AdminControlledEcosystemReserve.sol'; +import {ReentrancyGuard} from './libs/ReentrancyGuard.sol'; +import {SafeERC20} from './libs/SafeERC20.sol'; + +/** + * @title AaveEcosystemReserve v2 + * @notice Stores ERC20 tokens of an ecosystem reserve, adding streaming capabilities. + * 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 + * 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; + + /*** Storage Properties ***/ + + /** + * @notice Counter for new stream ids. + */ + uint256 private _nextStreamId; + + /** + * @notice The stream objects identifiable by their unsigned integer ids. + */ + mapping(uint256 => Stream) private _streams; + + /*** Modifiers ***/ + + /** + * @dev Throws if the caller is not the funds admin of the recipient of the stream. + */ + modifier onlyAdminOrRecipient(uint256 streamId) { + require( + msg.sender == _fundsAdmin || 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. + */ + modifier streamExists(uint256 streamId) { + require(_streams[streamId].isEntity, 'stream does not exist'); + _; + } + + /*** Contract Logic Starts Here */ + + function initialize(address fundsAdmin) external initializer { + _nextStreamId = 100000; + _setFundsAdmin(fundsAdmin); + } + + /*** 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. + * @dev Throws if the id does not point to a valid stream. + * @param streamId The id of the stream to query. + * @notice Returns the stream object. + */ + function getStream(uint256 streamId) + external + view + streamExists(streamId) + returns ( + address sender, + address recipient, + uint256 deposit, + address tokenAddress, + uint256 startTime, + uint256 stopTime, + uint256 remainingBalance, + uint256 ratePerSecond + ) + { + sender = _streams[streamId].sender; + recipient = _streams[streamId].recipient; + deposit = _streams[streamId].deposit; + tokenAddress = _streams[streamId].tokenAddress; + 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. + * @notice Returns the time delta in seconds. + */ + function deltaOf(uint256 streamId) public view streamExists(streamId) returns (uint256 delta) { + 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 { + uint256 recipientBalance; + uint256 withdrawalAmount; + uint256 senderBalance; + } + + /** + * @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. + * @notice Returns the total funds allocated to `who` as uint256. + */ + function balanceOf(uint256 streamId, address who) + public + view + streamExists(streamId) + returns (uint256 balance) + { + Stream memory stream = _streams[streamId]; + BalanceOfLocalVars memory vars; + + uint256 delta = deltaOf(streamId); + vars.recipientBalance = delta * stream.ratePerSecond; + + /* + * 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.withdrawalAmount = stream.deposit - stream.remainingBalance; + vars.recipientBalance = vars.recipientBalance - vars.withdrawalAmount; + } + + if (who == stream.recipient) return vars.recipientBalance; + if (who == stream.sender) { + vars.senderBalance = stream.remainingBalance - vars.recipientBalance; + return vars.senderBalance; + } + return 0; + } + + /*** Public Effects & Interactions Functions ***/ + + struct CreateStreamLocalVars { + uint256 duration; + uint256 ratePerSecond; + } + + /** + * @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. + * Throws if the deposit is 0. + * Throws if the start time is before `block.timestamp`. + * Throws if the stop time is before the start time. + * Throws if the duration calculation has a math error. + * Throws if the deposit is smaller than 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 next stream id calculation has a math error. + * Throws if the contract is not allowed to transfer enough tokens. + * Throws if there is a token transfer failure. + * @param recipient The address towards which the money is streamed. + * @param deposit The amount of money to be streamed. + * @param tokenAddress The ERC20 token to use as streaming currency. + * @param startTime The unix timestamp for when the stream starts. + * @param stopTime The unix timestamp for when the stream stops. + * @notice Returns the uint256 id of the newly created stream. + */ + function createStream( + address recipient, + uint256 deposit, + address tokenAddress, + 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 != msg.sender, 'stream to the caller'); + require(deposit > 0, 'deposit is zero'); + require(startTime >= block.timestamp, 'start time before block.timestamp'); + require(stopTime > startTime, 'stop time before the start time'); + + CreateStreamLocalVars memory vars; + vars.duration = stopTime - startTime; + + /* Without this, the rate per second would be zero. */ + require(deposit >= vars.duration, 'deposit smaller than time delta'); + + /* This condition avoids dealing with remainders */ + require(deposit % vars.duration == 0, 'deposit not multiple of time delta'); + + vars.ratePerSecond = deposit / vars.duration; + + /* Create and store the stream object. */ + uint256 streamId = _nextStreamId; + _streams[streamId] = Stream({ + remainingBalance: deposit, + deposit: deposit, + isEntity: true, + ratePerSecond: vars.ratePerSecond, + recipient: recipient, + sender: address(this), + startTime: startTime, + stopTime: stopTime, + tokenAddress: tokenAddress + }); + + /* Increment the next stream id. */ + _nextStreamId++; + + emit CreateStream( + streamId, + address(this), + recipient, + deposit, + tokenAddress, + startTime, + stopTime + ); + return streamId; + } + + /** + * @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 funds admin 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) + onlyAdminOrRecipient(streamId) + returns (bool) + { + require(amount > 0, 'amount is zero'); + Stream memory stream = _streams[streamId]; + + uint256 balance = balanceOf(streamId, stream.recipient); + require(balance >= amount, 'amount exceeds the available balance'); + + _streams[streamId].remainingBalance = stream.remainingBalance - amount; + + if (_streams[streamId].remainingBalance == 0) delete _streams[streamId]; + + IERC20(stream.tokenAddress).safeTransfer(stream.recipient, amount); + emit WithdrawFromStream(streamId, stream.recipient, amount); + return true; + } + + /** + * @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. + * Throws if the caller is not the funds admin or the recipient of the stream. + * Throws if there is a token transfer failure. + * @param streamId The id of the stream to cancel. + * @notice Returns bool true=success, otherwise false. + */ + function cancelStream(uint256 streamId) + external + nonReentrant + streamExists(streamId) + onlyAdminOrRecipient(streamId) + returns (bool) + { + Stream memory stream = _streams[streamId]; + uint256 senderBalance = balanceOf(streamId, stream.sender); + uint256 recipientBalance = balanceOf(streamId, stream.recipient); + + delete _streams[streamId]; + + IERC20 token = IERC20(stream.tokenAddress); + if (recipientBalance > 0) token.safeTransfer(stream.recipient, recipientBalance); + + emit CancelStream(streamId, stream.sender, stream.recipient, senderBalance, recipientBalance); + return true; + } +} diff --git a/contracts/treasury/AdminControlledEcosystemReserve.sol b/contracts/treasury/AdminControlledEcosystemReserve.sol new file mode 100644 index 00000000..27dcf053 --- /dev/null +++ b/contracts/treasury/AdminControlledEcosystemReserve.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.10; + +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {IAdminControlledEcosystemReserve} from './interfaces/IAdminControlledEcosystemReserve.sol'; +import {VersionedInitializable} from './libs/VersionedInitializable.sol'; +import {SafeERC20} from './libs/SafeERC20.sol'; +import {ReentrancyGuard} from './libs/ReentrancyGuard.sol'; +import {Address} from './libs/Address.sol'; + +/** + * @title AdminControlledEcosystemReserve + * @notice Stores ERC20 tokens, and allows to dispose of them via approval or transfer dynamics + * Adapted to be an implementation of a transparent proxy + * @dev Done abstract to add an `initialize()` function on the child, with `initializer` modifier + * @author BGD Labs + **/ +abstract contract AdminControlledEcosystemReserve is + VersionedInitializable, + IAdminControlledEcosystemReserve +{ + using SafeERC20 for IERC20; + using Address for address payable; + + address internal _fundsAdmin; + + uint256 public constant REVISION = 4; + + /// @inheritdoc IAdminControlledEcosystemReserve + address public constant ETH_MOCK_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + modifier onlyFundsAdmin() { + require(msg.sender == _fundsAdmin, 'ONLY_BY_FUNDS_ADMIN'); + _; + } + + function getRevision() internal pure override returns (uint256) { + return REVISION; + } + + /// @inheritdoc IAdminControlledEcosystemReserve + function getFundsAdmin() external view returns (address) { + return _fundsAdmin; + } + + /// @inheritdoc IAdminControlledEcosystemReserve + function approve( + IERC20 token, + address recipient, + uint256 amount + ) external onlyFundsAdmin { + token.safeApprove(recipient, amount); + } + + /// @inheritdoc IAdminControlledEcosystemReserve + function transfer( + IERC20 token, + address recipient, + uint256 amount + ) external onlyFundsAdmin { + require(recipient != address(0), 'INVALID_0X_RECIPIENT'); + + if (address(token) == ETH_MOCK_ADDRESS) { + payable(recipient).sendValue(amount); + } else { + token.safeTransfer(recipient, amount); + } + } + + /// @dev needed in order to receive ETH from the Aave v1 ecosystem reserve + receive() external payable {} + + function _setFundsAdmin(address admin) internal { + _fundsAdmin = admin; + emit NewFundsAdmin(admin); + } +} diff --git a/contracts/treasury/interfaces/IAaveEcosystemReserveController.sol b/contracts/treasury/interfaces/IAaveEcosystemReserveController.sol new file mode 100644 index 00000000..bd8131e7 --- /dev/null +++ b/contracts/treasury/interfaces/IAaveEcosystemReserveController.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; + +interface IAaveEcosystemReserveController { + /** + * @notice Proxy function for ERC20's approve(), pointing to a specific collector contract + * @param collector The collector contract with funds (Aave ecosystem reserve) + * @param token The asset address + * @param recipient Allowance's recipient + * @param amount Allowance to approve + **/ + function approve( + address collector, + IERC20 token, + address recipient, + uint256 amount + ) external; + + /** + * @notice Proxy function for ERC20's transfer(), pointing to a specific collector contract + * @param collector The collector contract with funds (Aave ecosystem reserve) + * @param token The asset address + * @param recipient Transfer's recipient + * @param amount Amount to transfer + **/ + function transfer( + address collector, + IERC20 token, + address recipient, + uint256 amount + ) external; + + /** + * @notice Proxy function to create a stream of token on a specific collector contract + * @param collector The collector contract with funds (Aave ecosystem reserve) + * @param recipient The recipient of the stream of token + * @param deposit Total amount to be streamed + * @param tokenAddress The ERC20 token to use as streaming asset + * @param startTime The unix timestamp for when the stream starts + * @param stopTime The unix timestamp for when the stream stops + * @return uint256 The stream id created + **/ + function createStream( + address collector, + address recipient, + uint256 deposit, + IERC20 tokenAddress, + uint256 startTime, + uint256 stopTime + ) external returns (uint256); + + /** + * @notice Proxy function to withdraw from a stream of token on a specific collector contract + * @param collector The collector contract with funds (Aave ecosystem reserve) + * @param streamId The id of the stream to withdraw tokens from + * @param funds Amount to withdraw + * @return bool If the withdrawal finished properly + **/ + function withdrawFromStream( + address collector, + uint256 streamId, + uint256 funds + ) external returns (bool); + + /** + * @notice Proxy function to cancel a stream of token on a specific collector contract + * @param collector The collector contract with funds (Aave ecosystem reserve) + * @param streamId The id of the stream to cancel + * @return bool If the cancellation happened correctly + **/ + function cancelStream(address collector, uint256 streamId) external returns (bool); +} diff --git a/contracts/treasury/interfaces/IAdminControlledEcosystemReserve.sol b/contracts/treasury/interfaces/IAdminControlledEcosystemReserve.sol new file mode 100644 index 00000000..3071f91a --- /dev/null +++ b/contracts/treasury/interfaces/IAdminControlledEcosystemReserve.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.10; + +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; + +interface IAdminControlledEcosystemReserve { + /** @notice Emitted when the funds admin changes + * @param fundsAdmin The new funds admin + **/ + event NewFundsAdmin(address indexed fundsAdmin); + + /** @notice Returns the mock ETH reference address + * @return address The address + **/ + function ETH_MOCK_ADDRESS() external pure returns (address); + + /** + * @notice Return the funds admin, only entity to be able to interact with this contract (controller of reserve) + * @return address The address of the funds admin + **/ + function getFundsAdmin() external view returns (address); + + /** + * @dev Function for the funds admin to give ERC20 allowance to other parties + * @param token The address of the token to give allowance from + * @param recipient Allowance's recipient + * @param amount Allowance to approve + **/ + function approve( + IERC20 token, + address recipient, + uint256 amount + ) external; + + /** + * @notice Function for the funds admin to transfer ERC20 tokens to other parties + * @param token The address of the token to transfer + * @param recipient Transfer's recipient + * @param amount Amount to transfer + **/ + function transfer( + IERC20 token, + address recipient, + uint256 amount + ) external; +} diff --git a/contracts/treasury/interfaces/IStreamable.sol b/contracts/treasury/interfaces/IStreamable.sol new file mode 100644 index 00000000..69e9f302 --- /dev/null +++ b/contracts/treasury/interfaces/IStreamable.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IStreamable { + struct Stream { + uint256 deposit; + uint256 ratePerSecond; + uint256 remainingBalance; + uint256 startTime; + uint256 stopTime; + address recipient; + address sender; + address tokenAddress; + bool isEntity; + } + + event CreateStream( + uint256 indexed streamId, + address indexed sender, + address indexed recipient, + uint256 deposit, + address tokenAddress, + uint256 startTime, + uint256 stopTime + ); + + event WithdrawFromStream(uint256 indexed streamId, address indexed recipient, uint256 amount); + + event CancelStream( + uint256 indexed streamId, + address indexed sender, + address indexed recipient, + uint256 senderBalance, + uint256 recipientBalance + ); + + function balanceOf(uint256 streamId, address who) external view returns (uint256 balance); + + function getStream(uint256 streamId) + external + view + returns ( + address sender, + address recipient, + uint256 deposit, + address token, + uint256 startTime, + uint256 stopTime, + uint256 remainingBalance, + uint256 ratePerSecond + ); + + function createStream( + address recipient, + uint256 deposit, + address tokenAddress, + uint256 startTime, + uint256 stopTime + ) external returns (uint256 streamId); + + function withdrawFromStream(uint256 streamId, uint256 funds) external returns (bool); + + function cancelStream(uint256 streamId) external returns (bool); + + function initialize(address fundsAdmin) external; +} diff --git a/contracts/treasury/libs/Address.sol b/contracts/treasury/libs/Address.sol new file mode 100644 index 00000000..32547d6c --- /dev/null +++ b/contracts/treasury/libs/Address.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (utils/Address.sol) + +pragma solidity ^0.8.1; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, 'Address: insufficient balance'); + + (bool success, ) = recipient.call{value: amount}(''); + require(success, 'Address: unable to send value, recipient may have reverted'); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, 'Address: low-level call failed'); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, 'Address: low-level call with value failed'); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require(address(this).balance >= value, 'Address: insufficient balance for call'); + require(isContract(target), 'Address: call to non-contract'); + + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) + internal + view + returns (bytes memory) + { + return functionStaticCall(target, data, 'Address: low-level static call failed'); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + require(isContract(target), 'Address: static call to non-contract'); + + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, 'Address: low-level delegate call failed'); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + require(isContract(target), 'Address: delegate call to non-contract'); + + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} diff --git a/contracts/treasury/libs/ReentrancyGuard.sol b/contracts/treasury/libs/ReentrancyGuard.sol new file mode 100644 index 00000000..d72d1723 --- /dev/null +++ b/contracts/treasury/libs/ReentrancyGuard.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor() { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, 'ReentrancyGuard: reentrant call'); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} diff --git a/contracts/treasury/libs/SafeERC20.sol b/contracts/treasury/libs/SafeERC20.sol new file mode 100644 index 00000000..ebc45c92 --- /dev/null +++ b/contracts/treasury/libs/SafeERC20.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/utils/SafeERC20.sol) + +pragma solidity ^0.8.0; + +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; +import {Address} from './Address.sol'; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using Address for address; + + function safeTransfer( + IERC20 token, + address to, + uint256 value + ) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value + ) internal { + _callOptionalReturn( + token, + abi.encodeWithSelector(token.transferFrom.selector, from, to, value) + ); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove( + IERC20 token, + address spender, + uint256 value + ) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + 'SafeERC20: approve from non-zero to non-zero allowance' + ); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + + function safeIncreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + uint256 newAllowance = token.allowance(address(this), spender) + value; + _callOptionalReturn( + token, + abi.encodeWithSelector(token.approve.selector, spender, newAllowance) + ); + } + + function safeDecreaseAllowance( + IERC20 token, + address spender, + uint256 value + ) internal { + unchecked { + uint256 oldAllowance = token.allowance(address(this), spender); + require(oldAllowance >= value, 'SafeERC20: decreased allowance below zero'); + uint256 newAllowance = oldAllowance - value; + _callOptionalReturn( + token, + abi.encodeWithSelector(token.approve.selector, spender, newAllowance) + ); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data, 'SafeERC20: low-level call failed'); + if (returndata.length > 0) { + // Return data is optional + require(abi.decode(returndata, (bool)), 'SafeERC20: ERC20 operation did not succeed'); + } + } +} diff --git a/contracts/treasury/libs/VersionedInitializable.sol b/contracts/treasury/libs/VersionedInitializable.sol new file mode 100644 index 00000000..64a3dd1c --- /dev/null +++ b/contracts/treasury/libs/VersionedInitializable.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +/** + * @title VersionedInitializable + * + * @dev Helper contract to support initializer functions. To use it, replace + * the constructor with a function that has the `initializer` modifier. + * WARNING: Unlike constructors, initializer functions must be manually + * invoked. This applies both to deploying an Initializable contract, as well + * as extending an Initializable contract via inheritance. + * WARNING: When used with inheritance, manual care must be taken to not invoke + * a parent initializer twice, or ensure that all initializers are idempotent, + * because this is not dealt with automatically as with constructors. + * + * @author Aave, inspired by the OpenZeppelin Initializable contract + */ +abstract contract VersionedInitializable { + /** + * @dev Indicates that the contract has been initialized. + */ + uint256 internal lastInitializedRevision = 0; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + uint256 revision = getRevision(); + require(revision > lastInitializedRevision, 'Contract instance has already been initialized'); + + lastInitializedRevision = revision; + + _; + } + + /// @dev returns the revision number of the contract. + /// Needs to be defined in the inherited class as a constant. + function getRevision() internal pure virtual returns (uint256); + + // Reserved storage space to allow for layout changes in the future. + uint256[50] private ______gap; +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 6898f5d9..f19a6522 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -32,11 +32,15 @@ const mainnetFork = MAINNET_FORK // export hardhat config const config: HardhatUserConfig = { solidity: { - version: '0.8.10', - settings: { - optimizer: { enabled: true, runs: 25000 }, - evmVersion: 'london', - }, + compilers: [ + { + version: '0.8.10', + settings: { + optimizer: { enabled: true, runs: 25000 }, + evmVersion: 'london', + }, + }, + ], }, tenderly: { project: TENDERLY_PROJECT,