The report is here
The repo is still private.
I can paste some code snippet.
The function we are verifying is the inherited withdraw function in ERC4626
function withdraw(
uint256 assets,
address receiver,
address owner
) public virtual returns (uint256 shares) {
shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up.
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
beforeWithdraw(assets, shares);
_burn(owner, shares);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
asset.safeTransfer(receiver, assets);
}
The beforeWithdraw
virtual function is overridden in the code below
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.13;
import {ERC20} from "solmate/tokens/ERC20.sol";
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
import {WETH} from "solmate/tokens/WETH.sol";
import {sc4626} from "../../src/sc4626.sol";
import {IEulerDToken} from "../../src/interfaces/euler/IEulerDToken.sol";
import {IEulerEToken} from "../../src/interfaces/euler/IEulerEToken.sol";
import {ICurvePool} from "../../src/interfaces/curve/ICurvePool.sol";
import {ILido} from "../../src/interfaces/lido/ILido.sol";
import {IwstETH} from "../../src/interfaces/lido/IwstETH.sol";
import {IMarkets} from "../../src/interfaces/euler/IMarkets.sol";
import {AggregatorV3Interface} from "../../src/interfaces/chainlink/AggregatorV3Interface.sol";
import {IVault} from "../../src/interfaces/balancer/IVault.sol";
import {IFlashLoanRecipient} from "../../src/interfaces/balancer/IFlashLoanRecipient.sol";
error InvalidTargetLtv();
error InvalidMaxLtv();
error InvalidFlashLoanCaller();
error InvalidSlippageTolerance();
error AdminZeroAddress();
contract scWETH is sc4626, IFlashLoanRecipient {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
event SlippageToleranceUpdated(address indexed user, uint256 newSlippageTolerance);
event MaxLtvUpdated(address indexed user, uint256 newMaxLtv);
event TargetLtvRatioUpdated(address indexed user, uint256 newTargetLtv);
event Harvest(uint256 profitSinceLastHarvest, uint256 performanceFee);
address public EULER;
// The Euler market contract
IMarkets public markets;
// Euler supply token for wstETH (ewstETH)
IEulerEToken public eToken;
// Euler debt token for WETH (dWETH)
IEulerDToken public dToken;
// Curve pool for ETH-stETH
ICurvePool public curvePool;
// Lido staking contract (stETH)
ILido public stEth;
IwstETH public wstETH;
WETH public weth;
// Chainlink pricefeed (stETH -> ETH)
AggregatorV3Interface public stEThToEthPriceFeed;
// Balancer vault for flashloans
IVault public balancerVault;
// total invested during last harvest/rebalance
uint256 public totalInvested;
// total profit generated for this vault
uint256 public totalProfit;
// The max loan to value(ltv) ratio for borrowing eth on euler with wsteth as collateral for the flashloan
uint256 public maxLtv = ...;
// the target ltv ratio at which we actually borrow (<= maxLtv)
uint256 public targetLtv = ...;
// slippage for curve swaps
uint256 public slippageTolerance = 0.99e18;
constructor(address _admin) sc4626(_admin, ERC20(address(weth)), "WETH Vault", "scWETH") {
if (_admin == address(0)) revert AdminZeroAddress();
ERC20(address(stEth)).safeApprove(address(wstETH), type(uint256).max);
ERC20(address(stEth)).safeApprove(address(curvePool), type(uint256).max);
ERC20(address(wstETH)).safeApprove(EULER, type(uint256).max);
ERC20(address(weth)).safeApprove(EULER, type(uint256).max);
ERC20(address(wstETH)).safeApprove(address(dToken), type(uint256).max);
ERC20(address(wstETH)).safeApprove(address(eToken), type(uint256).max);
// Enter the euler collateral market (collateral's address, *not* the eToken address) ,
markets.enterMarket(0, address(wstETH));
}
/// @param amount : amount of asset to withdraw into the vault
function withdrawToVault(uint256 amount) external onlyRole(KEEPER_ROLE) {
_withdrawToVault(amount);
}
//////////////////// VIEW METHODS //////////////////////////
function totalAssets() public view override returns (uint256 assets) {
// value of the supplied collateral in eth terms using chainlink oracle
assets = totalCollateralSupplied();
// account for slippage losses
assets = assets.mulWadDown(slippageTolerance);
// add float
assets += asset.balanceOf(address(this));
// subtract the debt
assets -= totalDebt();
}
// total wstETH supplied as collateral (in ETH terms)
function totalCollateralSupplied() public view returns (uint256) {
return wstEthToEth(eToken.balanceOfUnderlying(address(this)));
}
// total eth borrowed
function totalDebt() public view returns (uint256) {
return dToken.balanceOf(address(this));
}
// returns the net LTV at which we have borrowed till now (1e18 = 100%)
function getLtv() public view returns (uint256 ltv) {
uint256 collateral = totalCollateralSupplied();
if (collateral > 0) {
// totalDebt / totalSupplied
ltv = totalDebt().divWadUp(collateral);
}
}
//////////////////// EXTERNAL METHODS //////////////////////////
// called after the flashLoan on _rebalancePosition
function receiveFlashLoan(address[] memory, uint256[] memory amounts, uint256[] memory, bytes memory userData)
external
{
if (msg.sender != address(balancerVault)) {
revert InvalidFlashLoanCaller();
}
// the amount flashloaned
uint256 flashLoanAmount = amounts[0];
// decode user data
(bool deposit, uint256 amount) = abi.decode(userData, (bool, uint256));
amount += flashLoanAmount;
// if flashloan received as part of a deposit
if (deposit) {
// unwrap eth
weth.withdraw(amount);
// stake to lido / eth => stETH
stEth.submit{value: amount}(address(0x00));
// wrap stETH
wstETH.wrap(stEth.balanceOf(address(this)));
// add wstETH liquidity on Euler
eToken.deposit(0, type(uint256).max);
// borrow enough weth from Euler to payback flashloan
dToken.borrow(0, flashLoanAmount);
}
// if flashloan received as part of a withdrawal
else {
// repay debt + withdraw collateral
if (flashLoanAmount >= totalDebt()) {
dToken.repay(0, type(uint256).max);
eToken.withdraw(0, type(uint256).max);
} else {
dToken.repay(0, flashLoanAmount);
eToken.withdraw(0, _ethToWstEth(amount).divWadDown(slippageTolerance));
}
// unwrap wstETH
uint256 stEthAmount = wstETH.unwrap(wstETH.balanceOf(address(this)));
// stEth to eth
(, int256 price,,,) = stEThToEthPriceFeed.latestRoundData();
uint256 expected = stEthAmount.mulWadDown(uint256(price));
// stETH to eth
curvePool.exchange(1, 0, stEthAmount, expected.mulWadDown(slippageTolerance));
// wrap eth
weth.deposit{value: address(this).balance}();
}
// payback flashloan
asset.safeTransfer(address(balancerVault), flashLoanAmount);
}
// need to be able to receive eth
receive() external payable {}
//////////////////// INTERNAL METHODS //////////////////////////
function _withdrawToVault(uint256 amount) internal {
uint256 ltv = getLtv();
uint256 debt = totalDebt();
uint256 flashLoanAmount = (debt - ltv.mulWadDown(amount)).divWadDown(1e18 - ltv);
address[] memory tokens = new address[](1);
tokens[0] = address(weth);
uint256[] memory amounts = new uint256[](1);
amounts[0] = flashLoanAmount;
// take flashloan
balancerVault.flashLoan(address(this), tokens, amounts, abi.encode(false, amount));
}
function beforeWithdraw(uint256 assets, uint256) internal override {
uint256 float = asset.balanceOf(address(this));
if (assets <= float) {
return;
}
uint256 missing = assets - float;
// needed otherwise counted as loss during harvest
totalInvested -= missing;
_withdrawToVault(missing);
}
}
I have mocked all the external contracts such as BalancerVault
, EToken
, DToken
, WstETH
, StETH
, CurvePool
, etc.
I have omitted some irrelevant functions. As you can see, the beforeWithdraw
function calls _withdrawToVault
, which invokes balancerVault.flashLoan
, and balancerVault.flashLoan
function calls receiveFlashLoan
, which calls functions on EToken
, DToken
, CurvePool
, WstETH, stETH, WETH etc. A lot of external contracts are involved