Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[submodule "lib/ERC3156"]
branch = "main"
path = "lib/ERC3156"
url = "https://github.com/alcueca/ERC3156"
[submodule "lib/forge-std"]
branch = "master"
path = "lib/forge-std"
Expand Down
1 change: 1 addition & 0 deletions lib/ERC3156
Submodule ERC3156 added at b4521a
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@prb/contracts/=lib/prb-contracts/src/
@prb/math/=lib/prb-math/src/
@prb/test/=lib/prb-test/src/
erc3156/=lib/ERC3156/
forge-std/=lib/forge-std/src/
solarray=lib/solarray/src
src/=src/
Expand Down
50 changes: 45 additions & 5 deletions src/SablierV2Comptroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,25 @@ import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol";
import { Events } from "./libraries/Events.sol";

/// @title SablierV2Comptroller
/// @dev This contract implements the ISablierV2Comptroller interface.
/// @dev This contract implements the {ISablierV2Comptroller} interface.
contract SablierV2Comptroller is
ISablierV2Comptroller, // one dependency
Adminable // one dependency
{
/*//////////////////////////////////////////////////////////////////////////
PUBLIC STORAGE
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2Comptroller
UD60x18 public override flashFee;

/*//////////////////////////////////////////////////////////////////////////
INTERNAL STORAGE
//////////////////////////////////////////////////////////////////////////*/

/// @dev ERC-20 assets that can be flash loaned.
mapping(IERC20 => bool) internal _flashAssets;

/// @dev Global fees mapped by ERC-20 asset addresses.
mapping(IERC20 => UD60x18) internal _protocolFees;

Expand All @@ -39,17 +49,47 @@ contract SablierV2Comptroller is
protocolFee = _protocolFees[asset];
}

/// @inheritdoc ISablierV2Comptroller
function isFlashLoanable(IERC20 asset) external view override returns (bool result) {
result = _flashAssets[asset];
}

/*//////////////////////////////////////////////////////////////////////////
PUBLIC NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2Comptroller
function setProtocolFee(IERC20 asset, UD60x18 newFee) external onlyAdmin {
function setFlashFee(UD60x18 newFlashFee) external override onlyAdmin {
// Effects: set the new flash fee.
UD60x18 oldFlashFee = flashFee;
flashFee = newFlashFee;

// Emit an event.
emit Events.SetFlashFee({ admin: msg.sender, oldFlashFee: oldFlashFee, newFlashFee: newFlashFee });
}

/// @inheritdoc ISablierV2Comptroller
function setProtocolFee(IERC20 asset, UD60x18 newProtocolFee) external override onlyAdmin {
// Effects: set the new global fee.
UD60x18 oldFee = _protocolFees[asset];
_protocolFees[asset] = newFee;
UD60x18 oldProtocolFee = _protocolFees[asset];
_protocolFees[asset] = newProtocolFee;

// Emit an event.
emit Events.SetProtocolFee({
admin: msg.sender,
asset: asset,
oldProtocolFee: oldProtocolFee,
newProtocolFee: newProtocolFee
});
}

/// @inheritdoc ISablierV2Comptroller
function toggleFlashAsset(IERC20 asset) external override onlyAdmin {
// Effects: enable the ERC-20 asset for flash loaning.
bool oldFlag = _flashAssets[asset];
_flashAssets[asset] = !oldFlag;

// Emit an event.
emit Events.SetProtocolFee({ admin: msg.sender, asset: asset, oldFee: oldFee, newFee: newFee });
emit Events.ToggleFlashAsset({ admin: msg.sender, asset: asset, newFlag: !oldFlag });
}
}
16 changes: 8 additions & 8 deletions src/SablierV2LockupLinear.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ import { Helpers } from "./libraries/Helpers.sol";
import { Status } from "./types/Enums.sol";
import { LockupAmounts, Broker, LockupCreateAmounts, Durations, LockupLinearStream, Range } from "./types/Structs.sol";

import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol";
import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol";
import { ISablierV2Lockup } from "./interfaces/ISablierV2Lockup.sol";
import { ISablierV2LockupLinear } from "./interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupRecipient } from "./interfaces/hooks/ISablierV2LockupRecipient.sol";
import { ISablierV2LockupSender } from "./interfaces/hooks/ISablierV2LockupSender.sol";
import { SablierV2Lockup } from "./SablierV2Lockup.sol";

/// @title SablierV2LockupLinear
/// @dev This contract implements the ISablierV2LockupLinear interface.
/// @dev This contract implements the {ISablierV2LockupLinear} interface.
contract SablierV2LockupLinear is
ISablierV2LockupLinear, // one dependency
SablierV2Lockup, // two dependencies
ERC721("SablierV2LockupLinear NFT", "SAB-V2-LOCKUP-LIN") // six dependencies
ERC721("SablierV2LockupLinear NFT", "SAB-V2-LOCKUP-LIN"), // six dependencies
SablierV2Lockup // ten dependencies
{
using SafeERC20 for IERC20;

Expand Down Expand Up @@ -70,7 +70,7 @@ contract SablierV2LockupLinear is
}

/// @inheritdoc ISablierV2LockupLinear
function getRange(uint256 streamId) external view returns (Range memory range) {
function getRange(uint256 streamId) external view override returns (Range memory range) {
range = _streams[streamId].range;
}

Expand All @@ -88,7 +88,7 @@ contract SablierV2LockupLinear is
return 0;
}

// No need for an assertion here, since the `getStreamedAmount` function checks that the deposit amount
// No need for an assertion here, since the {getStreamedAmount} function checks that the deposit amount
// is greater than or equal to the streamed amount.
unchecked {
returnableAmount = _streams[streamId].amounts.deposit - getStreamedAmount(streamId);
Expand Down Expand Up @@ -162,7 +162,7 @@ contract SablierV2LockupLinear is
assert(streamedAmountUd.lte(depositAmount));

// Casting to uint128 is safe thanks to the assertion above.
streamedAmount = uint128(streamedAmountUd.unwrap());
streamedAmount = uint128(streamedAmountUd.intoUint256());
}
}

Expand Down Expand Up @@ -215,7 +215,7 @@ contract SablierV2LockupLinear is
range.start = uint40(block.timestamp);

// Calculate the cliff time and the stop time. It is safe to use unchecked arithmetic because the
// `_createWithRange` function will nonetheless check that the stop time is greater than or equal to the
// {_createWithRange} function will nonetheless check that the stop time is greater than or equal to the
// cliff time, and also that the cliff time is greater than or equal to the start time.
unchecked {
range.cliff = range.start + durations.cliff;
Expand Down
12 changes: 6 additions & 6 deletions src/SablierV2LockupPro.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/casting/Uint40.
import { sd, SD59x18 } from "@prb/math/SD59x18.sol";
import { UD60x18 } from "@prb/math/UD60x18.sol";

import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol";
import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol";
import { ISablierV2Lockup } from "./interfaces/ISablierV2Lockup.sol";
import { ISablierV2LockupPro } from "./interfaces/ISablierV2LockupPro.sol";
Expand All @@ -20,14 +21,13 @@ import { Events } from "./libraries/Events.sol";
import { Helpers } from "./libraries/Helpers.sol";
import { Status } from "./types/Enums.sol";
import { Broker, LockupCreateAmounts, LockupProStream, Segment } from "./types/Structs.sol";
import { SablierV2Lockup } from "./SablierV2Lockup.sol";

/// @title SablierV2LockupPro
/// @dev This contract implements the ISablierV2LockupPro interface.
/// @dev This contract implements the {ISablierV2LockupPro} interface.
contract SablierV2LockupPro is
ISablierV2LockupPro, // one dependency
SablierV2Lockup, // two dependencies
ERC721("Sablier V2 Pro NFT", "SAB-V2-PRO") // six dependencies
ERC721("Sablier V2 Pro NFT", "SAB-V2-PRO"), // six dependencies
SablierV2Lockup // ten dependencies
{
using CastingUint128 for uint128;
using CastingUint40 for uint40;
Expand Down Expand Up @@ -87,13 +87,13 @@ contract SablierV2LockupPro is
}

/// @inheritdoc ISablierV2Lockup
function getReturnableAmount(uint256 streamId) external view returns (uint128 returnableAmount) {
function getReturnableAmount(uint256 streamId) external view override returns (uint128 returnableAmount) {
// If the stream is null, return zero.
if (_streams[streamId].status == Status.NULL) {
return 0;
}

// No need for an assertion here, since the `getStreamedAmount` function checks that the deposit amount
// No need for an assertion here, since the {getStreamedAmount} function checks that the deposit amount
// is greater than or equal to the streamed amount.
unchecked {
returnableAmount = _streams[streamId].amounts.deposit - getStreamedAmount(streamId);
Expand Down
10 changes: 5 additions & 5 deletions src/SablierV2.sol → src/abstracts/SablierV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { IERC20 } from "@prb/contracts/token/erc20/IERC20.sol";
import { SafeERC20 } from "@prb/contracts/token/erc20/SafeERC20.sol";
import { UD60x18 } from "@prb/math/UD60x18.sol";

import { ISablierV2 } from "./interfaces/ISablierV2.sol";
import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol";
import { Errors } from "./libraries/Errors.sol";
import { Events } from "./libraries/Events.sol";
import { ISablierV2 } from "../interfaces/ISablierV2.sol";
import { ISablierV2Comptroller } from "../interfaces/ISablierV2Comptroller.sol";
import { Errors } from "../libraries/Errors.sol";
import { Events } from "../libraries/Events.sol";

/// @title SablierV2
/// @dev Abstract contract that implements the ISablierV2 interface.
/// @dev Abstract contract that implements the {ISablierV2} interface.
abstract contract SablierV2 is
ISablierV2, // no dependencies
Adminable // one dependency
Expand Down
162 changes: 162 additions & 0 deletions src/abstracts/SablierV2FlashLoan.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: LGPL-3.0
pragma solidity >=0.8.13;

import { IERC20 } from "@prb/contracts/token/erc20/IERC20.sol";
import { SafeERC20 } from "@prb/contracts/token/erc20/SafeERC20.sol";
import { ud } from "@prb/math/UD60x18.sol";
import { IERC3156FlashBorrower } from "erc3156/contracts/interfaces/IERC3156FlashBorrower.sol";
import { IERC3156FlashLender } from "erc3156/contracts/interfaces/IERC3156FlashLender.sol";

import { Errors } from "../libraries/Errors.sol";
import { Events } from "../libraries/Events.sol";
import { SablierV2 } from "./SablierV2.sol";

/// @dev Abstract contract that implements the {IERC3156FlashLender} interface.
abstract contract SablierV2FlashLoan is
IERC3156FlashLender, // no dependencies
SablierV2 // three dependencies
{
using SafeERC20 for IERC20;

/*//////////////////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////////////////*/

bytes32 internal constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");

/*//////////////////////////////////////////////////////////////////////////
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice The amount of fees to charge for a hypothetical flash loan amount.
///
/// @dev You might notice a bit of a terminology clash here, since the ERC-3156 standard refers to the "flash fee"
/// as an amount, whereas the flash fee queried from the comptroller is a percentage. To avoid any confusion, the
/// "amount" suffix is always appended to variables that represent amounts in this code base, but in this particular
/// context, the name be kept unchanged to comply with the ERC.
///
/// Requirements:
/// - The ERC-20 asset must be flash loanable.
///
/// @param asset The ERC-20 asset to flash loan.
/// @param amount The amount of `asset` flash loaned.
/// @return fee The amount of `asset` to charge for the loan on top of the returned principal.
function flashFee(address asset, uint256 amount) public view override returns (uint256 fee) {
// Checks: the ERC-20 asset is flash loanable.
if (!comptroller.isFlashLoanable(IERC20(asset))) {
revert Errors.SablierV2FlashLoan_AssetNotFlashLoanable(IERC20(asset));
}

// Calculate the flash fee.
fee = ud(amount).mul(comptroller.flashFee()).intoUint256();
}

/// @notice The amount of ERC-20 assets available to be flash loaned.
/// @dev If the ERC-20 asset is not flash loanable, this function returns zero.
/// @param asset The address of the ERC-20 asset to make the query for.
/// @return amount The amount of `asset` that can be flash loaned.
function maxFlashLoan(address asset) external view override returns (uint256 amount) {
// The default value is zero, so it doesn't have to be explicitly set.
if (comptroller.isFlashLoanable(IERC20(asset))) {
amount = IERC20(asset).balanceOf(address(this));
}
}

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice Allows smart contracts to access the entire liquidity of the Sablier V2 contract within one
/// transaction as long as the principal plus a flash fee is returned.
///
/// @dev Emits a {FlashLoan} event.
///
/// Requirements:
/// - All from {flashFee}.
/// - `amount` must be less than 2^128.
/// - `fee` must be less than 2^128.
/// - `amount` must not exceed the liquidity available for `asset`.
/// - `msg.sender` must allow this contract to spend at least `amount + fee` assets.
/// - `receiver` implementation of {ISablierV2FlashBorrower-onFlashLoan} must return `CALLBACK_SUCCESS`.
///
/// @param receiver The receiver of the flash loaned assets, and the receiver of the callback.
/// @param asset The address of the ERC-20 asset to use for flash borrowing.
/// @param amount The amount of `asset` to flash loan.
/// @param data Arbitrary data structure, intended to contain user-defined parameters.
/// @return success `true` on success.
function flashLoan(
IERC3156FlashBorrower receiver,
address asset,
uint256 amount,
bytes calldata data
) external override returns (bool success) {
// Checks: the amount is less than 2^128. This prevents the below calculations from overflowing.
if (amount > type(uint128).max) {
revert Errors.SablierV2FlashLoan_AmountTooHigh(amount);
}

// Calculate the flash fee. This also checks that the ERC-20 asset is flash loanable.
uint256 fee = flashFee(asset, amount);

// Checks: the calculated fee is less than 2^128. This check can fail only when the comptroller flash fee
// is set to an abnormally high value.
if (fee > type(uint128).max) {
revert Errors.SablierV2FlashLoan_FeeTooHigh(fee);
}

// Checks: the amount flash loaned is not greater than the current asset balance of the contract.
uint256 initialBalance = IERC20(asset).balanceOf(address(this));
if (amount > initialBalance) {
revert Errors.SablierV2FlashLoan_InsufficientAssetLiquidity({
asset: IERC20(asset),
amountAvailable: initialBalance,
amountRequested: amount
});
}

// Interactions: perform the ERC-20 transfer to flash loan the assets to the borrower.
IERC20(asset).safeTransfer({ to: address(receiver), amount: amount });

// Interactions: perform the borrower callback.
bytes32 response = receiver.onFlashLoan({
initiator: msg.sender,
token: asset,
amount: amount,
fee: fee,
data: data
});

// Checks: the response matches the expected callback success hash.
if (response != CALLBACK_SUCCESS) {
revert Errors.SablierV2FlashLoan_FlashBorrowFail();
}

uint256 returnAmount;

// Using unchecked arithmetic here because the checks above prevent these calculations from overflowing.
unchecked {
// Effects: record the flash fee amount in the protocol revenues. The casting to uint128 is safe thanks
// to the check at the start of the function.
_protocolRevenues[IERC20(asset)] += uint128(fee);

// Calculate the amount that the borrower must return.
returnAmount = amount + fee;
}

// Interactions: perform the ERC-20 transfer to get the principal back plus the fee.
IERC20(asset).safeTransferFrom({ from: address(receiver), to: address(this), amount: returnAmount });

// Emit an event.
emit Events.FlashLoan({
initiator: msg.sender,
receiver: receiver,
asset: IERC20(asset),
amount: amount,
feeAmount: fee,
data: data
});

// Set the success flag.
success = true;
}
}
Loading