Skip to content

Commit ffae958

Browse files
feat(bob): implement enterWithNativeToken function (#1442)
* feat(bob): implement enterWithNativeToken function * chore(adapter): rename variables style(bob): fix import errors chore: add solhint config to bob * refactor: minor improvements in Bob contract docs: update comments * chore: reorder modifiers * fix(bob): use safeTransfer for native token transfer --------- Co-authored-by: smol-ninja <shubhamy2015@gmail.com>
1 parent 261af5a commit ffae958

File tree

17 files changed

+401
-95
lines changed

17 files changed

+401
-95
lines changed

bob/.solhint.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"extends": "solhint:recommended",
3+
"rules": {
4+
"avoid-low-level-calls": "off",
5+
"code-complexity": ["error", 11],
6+
"compiler-version": ["error", ">=0.8.22"],
7+
"func-visibility": ["error", { "ignoreConstructors": true }],
8+
"function-max-lines": "off",
9+
"gas-increment-by-one": "off",
10+
"gas-indexed-events": "off",
11+
"gas-small-strings": "off",
12+
"gas-strict-inequalities": "off",
13+
"gas-struct-packing": "off",
14+
"imports-order": "warn",
15+
"import-path-check": "off",
16+
"max-line-length": ["error", 128],
17+
"max-states-count": ["warn", 20],
18+
"no-empty-blocks": "off",
19+
"no-unused-import": "error",
20+
"not-rely-on-time": "off",
21+
"use-natspec": "off",
22+
"var-name-mixedcase": "warn"
23+
}
24+
}

bob/scripts/solidity/DeployBob.s.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ contract DeployBob is BaseScript, LidoAdapterUtils {
2525
sablierBob: address(bob),
2626
curvePool: getCurvePool(),
2727
lidoWithdrawalQueue: getLidoWithdrawalQueue(),
28-
stETH: getStETH(),
29-
stETH_ETH_Oracle: getStETH_ETHOracle(),
30-
wETH: getWETH(),
31-
wstETH: getWSTETH(),
28+
steth: getSteth(),
29+
stethEthOracle: getStethEthOracle(),
30+
weth: getWeth(),
31+
wsteth: getWsteth(),
3232
initialSlippageTolerance: INITIAL_SLIPPAGE_TOLERANCE,
3333
initialYieldFee: INITIAL_YIELD_FEE
3434
});

bob/scripts/solidity/DeployDeterministicBob.s.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ contract DeployDeterministicBob is BaseScript, LidoAdapterUtils {
2727
sablierBob: address(bob),
2828
curvePool: getCurvePool(),
2929
lidoWithdrawalQueue: getLidoWithdrawalQueue(),
30-
stETH: getStETH(),
31-
stETH_ETH_Oracle: getStETH_ETHOracle(),
32-
wETH: getWETH(),
33-
wstETH: getWSTETH(),
30+
steth: getSteth(),
31+
stethEthOracle: getStethEthOracle(),
32+
weth: getWeth(),
33+
wsteth: getWsteth(),
3434
initialSlippageTolerance: INITIAL_SLIPPAGE_TOLERANCE,
3535
initialYieldFee: INITIAL_YIELD_FEE
3636
});

bob/scripts/solidity/LidoAdapterUtils.s.sol

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,37 +28,36 @@ abstract contract LidoAdapterUtils {
2828
}
2929
}
3030

31-
function getStETH() internal view returns (address stETH) {
31+
function getSteth() internal view returns (address steth) {
3232
if (block.chainid == ChainId.ETHEREUM) {
33-
stETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
33+
steth = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
3434
} else if (block.chainid == ChainId.SEPOLIA) {
35-
stETH = 0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af;
35+
steth = 0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af;
3636
}
3737
}
3838

39-
function getStETH_ETHOracle() internal view returns (address stETH_ETH_Oracle) {
39+
function getStethEthOracle() internal view returns (address stethEthOracle) {
4040
if (block.chainid == ChainId.ETHEREUM) {
4141
// Chainlink stETH/ETH feed on mainnet.
42-
stETH_ETH_Oracle = 0x86392dC19c0b719886221c78AB11eb8Cf5c52812;
42+
stethEthOracle = 0x86392dC19c0b719886221c78AB11eb8Cf5c52812;
4343
} else if (block.chainid == ChainId.SEPOLIA) {
4444
// Dummy since there is no stETH/ETH feed on Sepolia.
45-
stETH_ETH_Oracle = address(0);
4645
}
4746
}
4847

49-
function getWETH() internal view returns (address wETH) {
48+
function getWeth() internal view returns (address weth) {
5049
if (block.chainid == ChainId.ETHEREUM) {
51-
wETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
50+
weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
5251
} else if (block.chainid == ChainId.SEPOLIA) {
53-
wETH = 0xC558DBdd856501FCd9aaF1E62eae57A9F0629a3c;
52+
weth = 0xC558DBdd856501FCd9aaF1E62eae57A9F0629a3c;
5453
}
5554
}
5655

57-
function getWSTETH() internal view returns (address wstETH) {
56+
function getWsteth() internal view returns (address wsteth) {
5857
if (block.chainid == ChainId.ETHEREUM) {
59-
wstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
58+
wsteth = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
6059
} else if (block.chainid == ChainId.SEPOLIA) {
61-
wstETH = 0xB82381A3fBD3FaFA77B3a7bE693342618240067b;
60+
wsteth = 0xB82381A3fBD3FaFA77B3a7bE693342618240067b;
6261
}
6362
}
6463
}

bob/src/SablierBob.sol

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22
pragma solidity >=0.8.22;
33

4-
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
54
import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
65
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
77
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
8-
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
98
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
9+
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
1010
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
1111
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
12-
import { SafeOracle } from "@sablier/evm-utils/src/libraries/SafeOracle.sol";
13-
import { SafeTokenSymbol } from "@sablier/evm-utils/src/libraries/SafeTokenSymbol.sol";
1412
import { Comptrollerable } from "@sablier/evm-utils/src/Comptrollerable.sol";
1513
import { ISablierComptroller } from "@sablier/evm-utils/src/interfaces/ISablierComptroller.sol";
16-
14+
import { SafeOracle } from "@sablier/evm-utils/src/libraries/SafeOracle.sol";
15+
import { SafeTokenSymbol } from "@sablier/evm-utils/src/libraries/SafeTokenSymbol.sol";
1716
import { SablierBobState } from "./abstracts/SablierBobState.sol";
1817
import { BobVaultShare } from "./BobVaultShare.sol";
18+
import { IWETH9 } from "./interfaces/external/IWETH9.sol";
1919
import { IBobVaultShare } from "./interfaces/IBobVaultShare.sol";
2020
import { ISablierBob } from "./interfaces/ISablierBob.sol";
2121
import { ISablierBobAdapter } from "./interfaces/ISablierBobAdapter.sol";
@@ -188,38 +188,31 @@ contract SablierBob is
188188
notNull(vaultId)
189189
onlyActive(vaultId)
190190
{
191-
// Check: the deposit amount is not zero.
192-
if (amount == 0) {
193-
revert Errors.SablierBob_DepositAmountZero(vaultId, msg.sender);
194-
}
195-
196-
// Effect: sync the price from oracle.
197-
_syncPriceFromOracle(vaultId);
198-
199-
// Check: the vault is still active after the price sync.
200-
_revertIfSettledOrExpired(vaultId);
201-
202-
// Cache storage variables.
203-
ISablierBobAdapter adapter = _vaults[vaultId].adapter;
204-
IERC20 token = _vaults[vaultId].token;
191+
// Enter the vault.
192+
_enter({ vaultId: vaultId, from: msg.sender, amount: amount, token: _vaults[vaultId].token });
193+
}
205194

206-
// Interaction: transfer tokens from caller to this contract or the adapter.
207-
if (address(adapter) != address(0)) {
208-
// Interaction: Transfer token from caller to the adapter.
209-
token.safeTransferFrom(msg.sender, address(adapter), amount);
195+
/// @inheritdoc ISablierBob
196+
function enterWithNativeToken(uint256 vaultId)
197+
external
198+
payable
199+
override
200+
nonReentrant
201+
notNull(vaultId)
202+
onlyActive(vaultId)
203+
{
204+
// Cache the vault's token.
205+
address token = address(_vaults[vaultId].token);
210206

211-
// Interaction: stake the tokens via the adapter.
212-
adapter.stake(vaultId, msg.sender, amount);
213-
} else {
214-
// Interaction: Transfer tokens from caller to this contract.
215-
token.safeTransferFrom(msg.sender, address(this), amount);
216-
}
207+
// Interaction: call the deposit function in the vault tokens assuming it follows the IWETH9 interface.
208+
// Otherwise, it will revert.
209+
IWETH9(token).deposit{ value: msg.value }();
217210

218-
// Interaction: mint share tokens to the caller.
219-
_vaults[vaultId].shareToken.mint(vaultId, msg.sender, amount);
211+
// Cast `msg.value` to `uint128`.
212+
uint128 amount = msg.value.toUint128();
220213

221-
// Log the deposit.
222-
emit Enter({ vaultId: vaultId, user: msg.sender, amountReceived: amount, sharesMinted: amount });
214+
// Enter the vault.
215+
_enter({ vaultId: vaultId, from: address(this), amount: amount, token: IERC20(token) });
223216
}
224217

225218
/// @inheritdoc ISablierBob
@@ -443,7 +436,56 @@ contract SablierBob is
443436
PRIVATE STATE-CHANGING FUNCTIONS
444437
//////////////////////////////////////////////////////////////////////////*/
445438

439+
/// @dev Common function to enter into a vault by depositing tokens into it and minting share tokens to caller.
440+
/// @param vaultId The ID of the vault to deposit into.
441+
/// @param from The address holding the vault token when calling this function. In case of native token deposits,
442+
/// the vault tokens are held by this contract.
443+
/// @param amount The amount of tokens to deposit.
444+
/// @param token The ERC-20 token accepted by the vault.
445+
function _enter(uint256 vaultId, address from, uint128 amount, IERC20 token) private {
446+
// Check: the deposit amount is not zero.
447+
if (amount == 0) {
448+
revert Errors.SablierBob_DepositAmountZero(vaultId, msg.sender);
449+
}
450+
451+
// Effect: sync the price from oracle.
452+
_syncPriceFromOracle(vaultId);
453+
454+
// Check: the vault is still active after the price sync.
455+
_revertIfSettledOrExpired(vaultId);
456+
457+
// Cache storage variables.
458+
ISablierBobAdapter adapter = _vaults[vaultId].adapter;
459+
460+
// If adapter is set, transfer tokens to the adapter.
461+
if (address(adapter) != address(0)) {
462+
// Interaction: transfer tokens to the adapter. Use `safeTransfer` for the native token path since
463+
// the contract already holds the wrapped tokens.
464+
if (from == address(this)) {
465+
token.safeTransfer(address(adapter), amount);
466+
} else {
467+
token.safeTransferFrom(from, address(adapter), amount);
468+
}
469+
470+
// Interaction: stake tokens via the adapter on behalf of the caller.
471+
adapter.stake(vaultId, msg.sender, amount);
472+
}
473+
// Otherwise, if `from` is `msg.sender`, transfer tokens to this contract. When this function is called by
474+
// `enterWithNativeToken`, the vault tokens are held by this contract already.
475+
else if (from == msg.sender) {
476+
// Interaction: transfer tokens from caller to this contract.
477+
token.safeTransferFrom(from, address(this), amount);
478+
}
479+
480+
// Interaction: mint share tokens to the caller.
481+
_vaults[vaultId].shareToken.mint(vaultId, msg.sender, amount);
482+
483+
// Log the deposit.
484+
emit Enter({ vaultId: vaultId, user: msg.sender, amountReceived: amount, sharesMinted: amount });
485+
}
486+
446487
/// @notice Private function that reverts if the vault is settled or expired.
488+
/// @param vaultId The ID of the vault.
447489
function _revertIfSettledOrExpired(uint256 vaultId) private view {
448490
if (_statusOf(vaultId) != Bob.Status.ACTIVE) {
449491
revert Errors.SablierBob_VaultNotActive(vaultId);

bob/src/SablierLidoAdapter.sol

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,21 @@ contract SablierLidoAdapter is
102102
/// @param sablierBob The address of the SablierBob contract.
103103
/// @param curvePool The address of the Curve stETH/ETH pool.
104104
/// @param lidoWithdrawalQueue The address of the Lido withdrawal queue contract.
105+
/// @param steth The address of the stETH contract.
106+
/// @param stethEthOracle The address of the Chainlink's stETH/ETH oracle.
107+
/// @param weth The address of the WETH contract.
108+
/// @param wsteth The address of the wstETH contract.
105109
/// @param initialSlippageTolerance The initial slippage tolerance for Curve swaps as UD60x18.
106110
/// @param initialYieldFee The initial yield fee as UD60x18.
107111
constructor(
108112
address initialComptroller,
109113
address sablierBob,
110114
address curvePool,
111115
address lidoWithdrawalQueue,
112-
address stETH,
113-
address stETH_ETH_Oracle,
114-
address wETH,
115-
address wstETH,
116+
address steth,
117+
address stethEthOracle,
118+
address weth,
119+
address wsteth,
116120
UD60x18 initialSlippageTolerance,
117121
UD60x18 initialYieldFee
118122
)
@@ -131,10 +135,10 @@ contract SablierLidoAdapter is
131135
SABLIER_BOB = sablierBob;
132136
CURVE_POOL = curvePool;
133137
LIDO_WITHDRAWAL_QUEUE = lidoWithdrawalQueue;
134-
STETH = stETH;
135-
STETH_ETH_ORACLE = stETH_ETH_Oracle;
136-
WETH = wETH;
137-
WSTETH = wstETH;
138+
STETH = steth;
139+
STETH_ETH_ORACLE = stethEthOracle;
140+
WETH = weth;
141+
WSTETH = wsteth;
138142

139143
// Effect: set the initial slippage tolerance.
140144
slippageTolerance = initialSlippageTolerance;
@@ -423,7 +427,7 @@ contract SablierLidoAdapter is
423427
// Effect: store the total WETH received for redemption calculations.
424428
_wethReceivedAfterUnstaking[vaultId] = amountReceivedFromUnstaking;
425429

426-
// Interaction: Transfer WETH to SablierBob for distribution.
430+
// Interaction: transfer WETH to SablierBob for distribution.
427431
IERC20(WETH).safeTransfer(SABLIER_BOB, amountReceivedFromUnstaking);
428432

429433
// Log the event.

bob/src/interfaces/ISablierBob.sol

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ interface ISablierBob is IComptrollerable, ISablierBobState {
122122
/// - Share tokens are minted 1:1 with the deposited amount.
123123
///
124124
/// Requirements:
125+
/// - `vaultId` must not reference a null vault.
125126
/// - The vault must have ACTIVE status.
126127
/// - `amount` must be greater than zero.
127128
/// - The caller must have approved this contract to transfer `amount` tokens.
@@ -130,6 +131,22 @@ interface ISablierBob is IComptrollerable, ISablierBobState {
130131
/// @param amount The amount of tokens to deposit.
131132
function enter(uint256 vaultId, uint128 amount) external;
132133

134+
/// @notice Enter into a vault by depositing native token (such as ETH, POL, etc.) into it and minting share tokens
135+
/// to the caller.
136+
///
137+
/// @dev Emits an {Enter} event.
138+
///
139+
/// Notes:
140+
/// - `msg.value` is used as the deposit amount.
141+
/// - See notes for {enter}.
142+
///
143+
/// Requirements:
144+
/// - See requirements for {enter}.
145+
/// - `msg.value` must be greater than zero and must not exceed `type(uint128).max`.
146+
///
147+
/// @param vaultId The ID of the vault to deposit into.
148+
function enterWithNativeToken(uint256 vaultId) external payable;
149+
133150
/// @notice Called by adapter when share tokens for a given vault are transferred between users. This is required
134151
/// for accounting of the yield generated by the adapter.
135152
///
@@ -163,6 +180,7 @@ interface ISablierBob is IComptrollerable, ISablierBobState {
163180
/// Lido queue is finalized.
164181
///
165182
/// Requirements:
183+
/// - `vaultId` must not reference a null vault.
166184
/// - Either block timestamp must be greater than or equal to the vault expiry or the latest price from the oracle
167185
/// must be greater than or equal to the target price.
168186
/// - The share balance of the caller must be greater than zero.
@@ -216,6 +234,7 @@ interface ISablierBob is IComptrollerable, ISablierBobState {
216234
/// anyone to settle vault when the price is above the target price.
217235
///
218236
/// Requirements:
237+
/// - `vaultId` must not reference a null vault.
219238
/// - The vault must have ACTIVE status.
220239
/// - The oracle must return a positive price.
221240
///
@@ -229,6 +248,7 @@ interface ISablierBob is IComptrollerable, ISablierBobState {
229248
/// @dev Emits an {UnstakeFromAdapter} event.
230249
///
231250
/// Requirements:
251+
/// - `vaultId` must not reference a null vault.
232252
/// - The adapter set in the vault must not be zero address.
233253
/// - Either block timestamp must be greater than or equal to the vault expiry or the latest price from the oracle
234254
/// must be greater than or equal to the target price.

bob/src/interfaces/external/ICurveStETHPool.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
2+
// solhint-disable func-name-mixedcase
23
pragma solidity >=0.8.22;
34

45
/// @title ICurveStETHPool

bob/tests/bob/Base.t.sol

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ abstract contract Base_Test is Assertions, Modifiers, Utils {
4040
MockCurvePool internal curvePool;
4141
MockLidoWithdrawalQueue internal lidoWithdrawalQueue;
4242
MockStETH internal steth;
43-
ChainlinkOracleWith18Decimals internal stETHETHOracle;
43+
ChainlinkOracleWith18Decimals internal stethEthOracle;
4444
MockWETH internal weth;
4545
MockWstETH internal wstEth;
4646

@@ -104,9 +104,9 @@ abstract contract Base_Test is Assertions, Modifiers, Utils {
104104
curvePool = new MockCurvePool(address(steth));
105105
lidoWithdrawalQueue = new MockLidoWithdrawalQueue();
106106

107-
// Deploy a stETH/ETH oracle mock returning ~1:1 (1e18 in 18 decimals).
108-
stETHETHOracle = new ChainlinkOracleWith18Decimals();
109-
stETHETHOracle.setPrice(STETH_ETH_ORACLE_PRICE);
107+
// Deploy the stETH/ETH oracle mock and set the oracle price.
108+
stethEthOracle = new ChainlinkOracleWith18Decimals();
109+
stethEthOracle.setPrice(STETH_ETH_ORACLE_PRICE);
110110

111111
// Fund Curve pool with ETH for swaps.
112112
vm.deal(address(curvePool), 10_000 ether);
@@ -125,10 +125,10 @@ abstract contract Base_Test is Assertions, Modifiers, Utils {
125125
sablierBob: address(bob),
126126
curvePool: address(curvePool),
127127
lidoWithdrawalQueue: address(lidoWithdrawalQueue),
128-
stETH: address(steth),
129-
stETH_ETH_Oracle: address(stETHETHOracle),
130-
wETH: address(weth),
131-
wstETH: address(wstEth),
128+
steth: address(steth),
129+
stethEthOracle: address(stethEthOracle),
130+
weth: address(weth),
131+
wsteth: address(wstEth),
132132
initialSlippageTolerance: SLIPPAGE_TOLERANCE,
133133
initialYieldFee: YIELD_FEE
134134
});

0 commit comments

Comments
 (0)