Skip to content
Open
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
100 changes: 100 additions & 0 deletions src/integrations/BaseBundler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.24;

import {IMulticall} from "./interfaces/IMulticall.sol";
import {SafeTransferLib, ERC20} from "./libraries/SafeTransferLib.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";

/// @dev The default value of the initiator of the multicall transaction is not the address zero to save gas.
address constant UNSET_INITIATOR = address(1);

/// @title BaseBundler
/// @author Morpho Labs
/// @custom:contact [email protected]
/// @notice Enables calling multiple functions in a single call to the same contract (self).
/// @dev Every bundler must inherit from this contract.
/// @dev Every bundler inheriting from this contract must have their external functions payable as they will be
/// delegate called by the `multicall` function (which is payable, and thus might pass a non-null ETH value). It is
/// recommended not to rely on `msg.value` as the same value can be reused for multiple calls.
abstract contract BaseBundler is IMulticall {
using SafeTransferLib for ERC20;

/* STORAGE */

/// @notice Keeps track of the bundler's latest bundle initiator.
/// @dev Also prevents interacting with the bundler outside of an initiated execution context.
address private _initiator = UNSET_INITIATOR;

/* MODIFIERS */

/// @dev Prevents a function to be called outside an initiated `multicall` context and protects a function from
/// being called by an unauthorized sender inside an initiated multicall context.
modifier protected() {
require(_initiator != UNSET_INITIATOR, ErrorsLib.UNINITIATED);
require(_isSenderAuthorized(), ErrorsLib.UNAUTHORIZED_SENDER);

_;
}

/* PUBLIC */

/// @notice Returns the address of the initiator of the multicall transaction.
/// @dev Specialized getter to prevent using `_initiator` directly.
function initiator() public view returns (address) {
return _initiator;
}

/* EXTERNAL */

/// @notice Executes a series of delegate calls to the contract itself.
/// @dev Locks the initiator so that the sender can uniquely be identified in callbacks.
/// @dev All functions delegatecalled must be `payable` if `msg.value` is non-zero.
function multicall(bytes[] memory data) external payable {
require(_initiator == UNSET_INITIATOR, ErrorsLib.ALREADY_INITIATED);

_initiator = msg.sender;

_multicall(data);

_initiator = UNSET_INITIATOR;
}

/* INTERNAL */

/// @dev Executes a series of delegate calls to the contract itself.
/// @dev All functions delegatecalled must be `payable` if `msg.value` is non-zero.
function _multicall(bytes[] memory data) internal {
for (uint256 i; i < data.length; ++i) {
(bool success, bytes memory returnData) = address(this)
.delegatecall(data[i]);

// No need to check that `address(this)` has code in case of success.
if (!success) _revert(returnData);
}
}

/// @dev Bubbles up the revert reason / custom error encoded in `returnData`.
/// @dev Assumes `returnData` is the return data of any kind of failing CALL to a contract.
function _revert(bytes memory returnData) internal pure {
uint256 length = returnData.length;
require(length > 0, ErrorsLib.CALL_FAILED);

assembly ("memory-safe") {
revert(add(32, returnData), length)
}
}

/// @dev Returns whether the sender of the call is authorized.
/// @dev Assumes to be inside a properly initiated `multicall` context.
function _isSenderAuthorized() internal view virtual returns (bool) {
return msg.sender == _initiator;
}

/// @dev Gives the max approval to `spender` to spend the given `asset` if not already approved.
/// @dev Assumes that `type(uint256).max` is large enough to never have to increase the allowance again.
function _approveMaxTo(address asset, address spender) internal {
if (ERC20(asset).allowance(address(this), spender) == 0) {
ERC20(asset).safeApprove(spender, type(uint256).max);
}
}
}
54 changes: 54 additions & 0 deletions src/integrations/SentimentBundler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.24;

import {WNativeBundler} from "./WNativeBundler.sol";
import {TransferBundler} from "./TransferBundler.sol";
import {ISentiment, Action} from "./interfaces/ISentiment.sol";
import {SafeTransferLib, ERC20} from "./libraries/SafeTransferLib.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";

/// @title SentimentBundler
/// @author Based on Morpho Labs' bundler pattern
/// @notice Simplified bundler contract for interacting with Sentiment protocol positions
/// @dev Inherits from WNativeBundler and TransferBundler to provide ETH wrapping and transfer functionality
contract SentimentBundler is WNativeBundler, TransferBundler {
using SafeTransferLib for ERC20;

/* IMMUTABLES */

/// @notice The Sentiment PositionManager contract
ISentiment public immutable POSITION_MANAGER;

/* CONSTRUCTOR */

constructor(
address positionManager,
address wNative
) WNativeBundler(wNative) {
require(positionManager != address(0), ErrorsLib.ZERO_ADDRESS);
POSITION_MANAGER = ISentiment(positionManager);
}

/* ACTIONS */

/// @notice Process a batch of actions on a position
/// @param position Position address
/// @param actions List of actions to process
function processBatch(
address position,
Action[] calldata actions
) external payable protected {
require(position != address(0), ErrorsLib.ZERO_ADDRESS);
require(actions.length > 0, ErrorsLib.EMPTY_ARRAY);

// Use delegatecall to preserve msg.sender
bytes memory data = abi.encodeWithSelector(
POSITION_MANAGER.processBatch.selector,
position,
actions
);
(bool success, bytes memory returnData) = address(POSITION_MANAGER)
.delegatecall(data);
if (!success) _revert(returnData);
}
}
74 changes: 74 additions & 0 deletions src/integrations/TransferBundler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.24;

import {BaseBundler} from "./BaseBundler.sol";
import {SafeTransferLib, ERC20} from "./libraries/SafeTransferLib.sol";
import {Math} from "./libraries/Math.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";

/// @title TransferBundler
/// @author Morpho Labs
/// @custom:contact [email protected]
/// @notice Enables transfer of ERC20 and native tokens.
/// @dev Assumes that any tokens left on the contract can be seized by anyone.
abstract contract TransferBundler is BaseBundler {
using SafeTransferLib for ERC20;

/* TRANSFER ACTIONS */

/// @notice Transfers the minimum between the given `amount` and the bundler's balance of native asset from the
/// bundler to `recipient`.
/// @dev If the minimum happens to be zero, the transfer is silently skipped.
/// @param recipient The address that will receive the native tokens.
/// @param amount The amount of native tokens to transfer. Capped at the bundler's balance.
function nativeTransfer(
address recipient,
uint256 amount
) external payable protected {
require(recipient != address(0), ErrorsLib.ZERO_ADDRESS);
require(recipient != address(this), ErrorsLib.BUNDLER_ADDRESS);

amount = Math.min(amount, address(this).balance);

if (amount == 0) return;

SafeTransferLib.safeTransferETH(recipient, amount);
}

/// @notice Transfers the minimum between the given `amount` and the bundler's balance of `asset` from the bundler
/// to `recipient`.
/// @dev If the minimum happens to be zero, the transfer is silently skipped.
/// @param asset The address of the ERC20 token to transfer.
/// @param recipient The address that will receive the tokens.
/// @param amount The amount of `asset` to transfer. Capped at the bundler's balance.
function erc20Transfer(
address asset,
address recipient,
uint256 amount
) external payable protected {
require(recipient != address(0), ErrorsLib.ZERO_ADDRESS);
require(recipient != address(this), ErrorsLib.BUNDLER_ADDRESS);

amount = Math.min(amount, ERC20(asset).balanceOf(address(this)));

if (amount == 0) return;

ERC20(asset).safeTransfer(recipient, amount);
}

/// @notice Transfers the given `amount` of `asset` from sender to this contract via ERC20 transferFrom.
/// @notice User must have given sufficient allowance to the Bundler to spend their tokens.
/// @param asset The address of the ERC20 token to transfer.
/// @param amount The amount of `asset` to transfer from the initiator. Capped at the initiator's balance.
function erc20TransferFrom(
address asset,
uint256 amount
) external payable protected {
address _initiator = initiator();
amount = Math.min(amount, ERC20(asset).balanceOf(_initiator));

require(amount != 0, ErrorsLib.ZERO_AMOUNT);

ERC20(asset).safeTransferFrom(_initiator, address(this), amount);
}
}
61 changes: 61 additions & 0 deletions src/integrations/WNativeBundler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.24;

import {BaseBundler} from "./BaseBundler.sol";
import {SafeTransferLib, ERC20} from "./libraries/SafeTransferLib.sol";
import {Math} from "./libraries/Math.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {IWNative} from "./interfaces/IWNative.sol";

/// @title WNativeBundler
/// @author Morpho Labs
/// @custom:contact [email protected]
/// @notice Bundler contract managing interactions with network's wrapped native token.
/// @notice "wrapped native" refers to forks of WETH.
abstract contract WNativeBundler is BaseBundler {
using SafeTransferLib for ERC20;

/* IMMUTABLES */

/// @dev The address of the wrapped native token contract.
address public immutable WRAPPED_NATIVE;

/* CONSTRUCTOR */

/// @param wNative The address of the wNative token contract.
constructor(address wNative) {
require(wNative != address(0), ErrorsLib.ZERO_ADDRESS);
WRAPPED_NATIVE = wNative;
}

/* FALLBACKS */

/// @notice Native tokens are received by the bundler and should be used afterwards.
/// @dev Allows the wrapped native contract to send native tokens to the bundler.
receive() external payable {}

/* ACTIONS */

/// @notice Wraps the given `amount` of the native token to wNative.
/// @notice Wrapped native tokens are received by the bundler and should be used afterwards.
/// @dev Initiator must have previously transferred their native tokens to the bundler.
/// @param amount The amount of native token to wrap. Capped at the bundler's native token balance.
function wrapNative(uint256 amount) external payable protected {
amount = Math.min(amount, address(this).balance);
require(amount != 0, ErrorsLib.ZERO_AMOUNT);
IWNative(WRAPPED_NATIVE).deposit{value: amount}();
}

/// @notice Unwraps the given `amount` of wNative to the native token.
/// @notice Unwrapped native tokens are received by the bundler and should be used afterwards.
/// @dev Initiator must have previously transferred their wrapped native tokens to the bundler.
/// @param amount The amount of wrapped native token to unwrap. Capped at the bundler's wNative balance.
function unwrapNative(uint256 amount) external payable protected {
amount = Math.min(
amount,
ERC20(WRAPPED_NATIVE).balanceOf(address(this))
);
require(amount != 0, ErrorsLib.ZERO_AMOUNT);
IWNative(WRAPPED_NATIVE).withdraw(amount);
}
}
12 changes: 12 additions & 0 deletions src/integrations/interfaces/IMulticall.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.24;

/// @title IMulticall
/// @author Morpho Labs
/// @custom:contact [email protected]
/// @notice Interface of Multicall.
interface IMulticall {
/// @notice Executes an ordered batch of delegatecalls to this contract.
/// @param data The ordered array of calldata to execute.
function multicall(bytes[] calldata data) external payable;
}
36 changes: 36 additions & 0 deletions src/integrations/interfaces/ISentiment.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.24;

/// @notice Operation type definitions that can be applied to a position
enum Operation {
NewPosition, // create2 a new position with a given type, no auth needed
// the following operations require msg.sender to be authorized
Exec, // execute arbitrary calldata on a position
Deposit, // Add collateral to a given position
Transfer, // transfer assets from the position to a external address
Approve, // allow a spender to transfer assets from a position
Repay, // decrease position debt
Borrow, // increase position debt
AddToken, // upsert collateral asset to position storage
RemoveToken // remove collateral asset from position storage
}

/// @title Action
/// @notice Generic data struct to create a common data container for all operation types
struct Action {
// operation type
Operation op;
// dynamic bytes data, interepreted differently across operation types
bytes data;
}

/// @title ISentiment
/// @author Based on Morpho Labs' bundler pattern
/// @notice Interface for interacting with Sentiment protocol's PositionManager
interface ISentiment {
/// @notice Procces a batch of actions on a given position
/// @dev only one position can be operated on in one txn, including creation
/// @param position Position address
/// @param actions List of actions to process
function processBatch(address position, Action[] calldata actions) external;
}
15 changes: 15 additions & 0 deletions src/integrations/interfaces/IWNative.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.24;

/// @title IWNative
/// @author Morpho Labs
/// @custom:contact [email protected]
/// @notice Interface for a wrapped native token (WETH9 alike).
interface IWNative {
/// @notice Deposit ETH to get WETH.
function deposit() external payable;

/// @notice Withdraw ETH from WETH.
/// @param amount The amount to withdraw.
function withdraw(uint256 amount) external;
}
Loading