Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/strategy/BtcVaultStrategy.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import {ManagedWithdrawReportedStrategy} from "./ManagedWithdrawRWAStrategy.sol";
import {BtcVaultToken} from "../token/BtcVaultToken.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {CollateralManagementLib} from "../libraries/CollateralManagementLib.sol";
import {CollateralViewLib} from "../libraries/CollateralViewLib.sol";
/// @title BtcVaultStrategy
/// @notice Multi-collateral BTC vault strategy with managed withdrawals
contract BtcVaultStrategy is ManagedWithdrawReportedStrategy {
using SafeTransferLib for address;
uint8 public constant COLLATERAL_DECIMALS = 8;
mapping(address => bool) public supportedAssets;
address[] public collateralTokens;
/// @notice Initialize the strategy
function initialize(
string calldata name_,
string calldata symbol_,
address roleManager_,
address manager_,
address sovaBTC_,
uint8 assetDecimals_,
bytes memory initData
) public virtual override {
super.initialize(name_, symbol_, roleManager_, manager_, sovaBTC_, assetDecimals_, initData);
require(assetDecimals_ == 8);
supportedAssets[sovaBTC_] = true;
collateralTokens.push(sovaBTC_);
}
/// @notice Deploy a new BtcVaultToken for this strategy
function _deployToken(string calldata name_, string calldata symbol_, address asset_, uint8 /* assetDecimals_ */)
internal
override
returns (address)
{
return address(new BtcVaultToken(name_, symbol_, asset_, address(this)));
}
/// @notice Add a new supported collateral token
function addCollateral(address token) external onlyManager {
CollateralManagementLib.addCollateral(token, supportedAssets, collateralTokens, COLLATERAL_DECIMALS);
}
/// @notice Remove a supported collateral token
function removeCollateral(address token) external onlyManager {
CollateralManagementLib.removeCollateral(token, asset, supportedAssets, collateralTokens);
}
/// @notice Deposit collateral directly to the strategy
function depositCollateral(address token, uint256 amount) external {
CollateralManagementLib.depositCollateral(token, amount, supportedAssets);
}
/// @notice Withdraw collateral to admin
function withdrawCollateral(address token, uint256 amount, address to) external onlyManager {
CollateralManagementLib.withdrawCollateral(token, amount, to, supportedAssets);
}
/// @notice Add sovaBTC for redemptions
function addLiquidity(uint256 amount) external onlyManager {
CollateralManagementLib.addLiquidity(asset, amount);
}
/// @notice Remove sovaBTC from strategy
function removeLiquidity(uint256 amount, address to) external onlyManager {
CollateralManagementLib.removeLiquidity(asset, amount, to);
}
/// @notice Approve token to withdraw assets during redemptions
function approveTokenWithdrawal(uint256 amount) external onlyManager {
asset.safeApprove(sToken, 0);
asset.safeApprove(sToken, amount);
}
/// @notice Get total collateral assets value
function totalCollateralAssets() external view returns (uint256) {
return CollateralViewLib.totalCollateralAssets(collateralTokens);
}
/// @notice Get balance of a specific collateral token
function collateralBalance(address token) external view returns (uint256) {
return CollateralViewLib.collateralBalance(token, supportedAssets);
}
/// @notice Get available sovaBTC balance for redemptions
function availableLiquidity() external view returns (uint256) {
return CollateralViewLib.availableLiquidity(asset);
}
/*//////////////////////////////////////////////////////////////
EIP-712 OVERRIDE
//////////////////////////////////////////////////////////////*/
/// @notice EIP-712 Type Hash Constants
bytes32 private constant EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
/**
* @notice Calculate the EIP-712 domain separator with correct contract name
* @return The domain separator
*/
function _domainSeparator() internal view override returns (bytes32) {
return keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes("BtcVaultStrategy")),
keccak256(bytes("V1")),
block.chainid,
address(this)
)
);
}
}
"
},
"src/strategy/ManagedWithdrawRWAStrategy.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import {ECDSA} from "solady/utils/ECDSA.sol";
import {ManagedWithdrawRWA} from "../token/ManagedWithdrawRWA.sol";
import {ReportedStrategy} from "./ReportedStrategy.sol";
/**
* @title ManagedWithdrawReportedStrategy
* @notice Extension of ReportedStrategy that deploys and configures ManagedWithdrawRWA tokens
*/
contract ManagedWithdrawReportedStrategy is ReportedStrategy {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error WithdrawalRequestExpired();
error WithdrawNonceReuse();
error WithdrawInvalidSignature();
error InvalidArrayLengths();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event WithdrawalNonceUsed(address indexed owner, uint96 nonce);
/*//////////////////////////////////////////////////////////////
EIP-712 DATA
//////////////////////////////////////////////////////////////*/
/// @notice Signature argument struct
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
// EIP-712 Type Hash Constants
bytes32 private constant EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 private constant WITHDRAWAL_REQUEST_TYPEHASH = keccak256(
"WithdrawalRequest(address owner,address to,uint256 shares,uint256 minAssets,uint96 nonce,uint96 expirationTime)"
);
/*//////////////////////////////////////////////////////////////
STATE
//////////////////////////////////////////////////////////////*/
/// @notice Struct to track withdrawal requests
struct WithdrawalRequest {
uint256 shares;
uint256 minAssets;
address owner;
uint96 nonce;
address to;
uint96 expirationTime;
}
// Tracking of used nonces
mapping(address => mapping(uint96 => bool)) public usedNonces;
/*//////////////////////////////////////////////////////////////
INITIALIZATION
//////////////////////////////////////////////////////////////*/
/**
* @notice Initialize the strategy with ManagedWithdrawRWA token
* @param name_ Name of the token
* @param symbol_ Symbol of the token
* @param roleManager_ Address of the role manager
* @param manager_ Address of the manager
* @param asset_ Address of the underlying asset
* @param assetDecimals_ Decimals of the asset
* @param initData Additional initialization data (unused)
*/
function initialize(
string calldata name_,
string calldata symbol_,
address roleManager_,
address manager_,
address asset_,
uint8 assetDecimals_,
bytes memory initData
) public virtual override {
super.initialize(name_, symbol_, roleManager_, manager_, asset_, assetDecimals_, initData);
}
/**
* @notice Deploy a new ManagedWithdrawRWA token
* @param name_ Name of the token
* @param symbol_ Symbol of the token
* @param asset_ Address of the underlying asset
* @param assetDecimals_ Decimals of the asset
*/
function _deployToken(string calldata name_, string calldata symbol_, address asset_, uint8 assetDecimals_)
internal
virtual
override
returns (address)
{
ManagedWithdrawRWA newToken = new ManagedWithdrawRWA(name_, symbol_, asset_, assetDecimals_, address(this));
return address(newToken);
}
/*//////////////////////////////////////////////////////////////
REDEMPTION LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Process a user-requested withdrawal
* @param request The withdrawal request
* @param userSig The signature of the request
* @return assets The amount of assets received
*/
function redeem(WithdrawalRequest calldata request, Signature calldata userSig)
external
onlyManager
returns (uint256 assets)
{
_validateRedeem(request);
// Verify signature
_verifySignature(request, userSig);
assets = ManagedWithdrawRWA(sToken).redeem(request.shares, request.to, request.owner, request.minAssets);
}
/**
* @notice Process a batch of user-requested withdrawals
* @param requests The withdrawal requests
* @param signatures The signatures of the requests
* @return assets The amount of assets received
*/
function batchRedeem(WithdrawalRequest[] calldata requests, Signature[] calldata signatures)
external
onlyManager
returns (uint256[] memory assets)
{
if (requests.length != signatures.length) revert InvalidArrayLengths();
uint256[] memory shares = new uint256[](requests.length);
address[] memory recipients = new address[](requests.length);
address[] memory owners = new address[](requests.length);
uint256[] memory minAssets = new uint256[](requests.length);
for (uint256 i = 0; i < requests.length;) {
_validateRedeem(requests[i]);
_verifySignature(requests[i], signatures[i]);
shares[i] = requests[i].shares;
recipients[i] = requests[i].to;
owners[i] = requests[i].owner;
minAssets[i] = requests[i].minAssets;
unchecked {
++i;
}
}
assets = ManagedWithdrawRWA(sToken).batchRedeemShares(shares, recipients, owners, minAssets);
}
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @notice Validate a withdrawal request's arguments and consume the nonce
* @param request The withdrawal request
*/
function _validateRedeem(WithdrawalRequest calldata request) internal {
if (request.expirationTime < block.timestamp) revert WithdrawalRequestExpired();
// Cache the nonce status to avoid duplicate storage read
mapping(uint96 => bool) storage userNonces = usedNonces[request.owner];
if (userNonces[request.nonce]) revert WithdrawNonceReuse();
// Consume the nonce
userNonces[request.nonce] = true;
emit WithdrawalNonceUsed(request.owner, request.nonce);
}
/**
* @notice Verify a signature using EIP-712
* @param request The withdrawal request
* @param signature The signature
*/
function _verifySignature(WithdrawalRequest calldata request, Signature calldata signature) internal view {
// Verify signature
bytes32 structHash = keccak256(
abi.encode(
WITHDRAWAL_REQUEST_TYPEHASH,
request.owner,
request.to,
request.shares,
request.minAssets,
request.nonce,
request.expirationTime
)
);
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash));
// Recover signer address from signature
address signer = ECDSA.recover(digest, signature.v, signature.r, signature.s);
// Verify the signer is the owner of the shares
if (signer != request.owner) revert WithdrawInvalidSignature();
}
/**
* @notice Calculate the EIP-712 domain separator
* @return The domain separator
*/
function _domainSeparator() internal view virtual returns (bytes32) {
return keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes("ManagedWithdrawReportedStrategy")),
keccak256(bytes("V1")),
block.chainid,
address(this)
)
);
}
}
"
},
"src/token/BtcVaultToken.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import {ManagedWithdrawRWA} from "./ManagedWithdrawRWA.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {IBtcVaultStrategy} from "../interfaces/IBtcVaultStrategy.sol";
/**
* @title BtcVaultToken
* @notice Multi-collateral BTC vault token that extends ManagedWithdrawRWA
* @dev Supports deposits of multiple BTC collateral types, redeems only in sovaBTC
*/
contract BtcVaultToken is ManagedWithdrawRWA {
using FixedPointMathLib for uint256;
using SafeTransferLib for address;
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error TokenNotSupported();
error InsufficientAmount();
error ZeroShares();
error StandardDepositDisabled();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event CollateralDeposited(
address indexed depositor, address indexed token, uint256 amount, uint256 shares, address indexed receiver
);
/*//////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
/// @notice Minimum deposit amount (0.001 BTC in 8 decimals)
uint256 public constant MIN_DEPOSIT = 1e5;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/**
* @notice Constructor
* @param name_ Token name
* @param symbol_ Token symbol
* @param sovaBTC_ Address of sovaBTC (redemption asset, 8 decimals)
* @param strategy_ Address of BtcVaultStrategy
*/
constructor(string memory name_, string memory symbol_, address sovaBTC_, address strategy_)
ManagedWithdrawRWA(name_, symbol_, sovaBTC_, 8, strategy_)
{
// ManagedWithdrawRWA handles all initialization
}
/*//////////////////////////////////////////////////////////////
MULTI-COLLATERAL DEPOSITS
//////////////////////////////////////////////////////////////*/
/**
* @notice Deposit BTC collateral tokens for shares
* @param token Address of the BTC collateral token
* @param amount Amount of collateral to deposit (8 decimals)
* @param receiver Address to receive shares
* @return shares Amount of shares minted (18 decimals)
*/
function depositCollateral(address token, uint256 amount, address receiver)
external
nonReentrant
returns (uint256 shares)
{
if (!IBtcVaultStrategy(strategy).isSupportedAsset(token)) revert TokenNotSupported();
if (amount < MIN_DEPOSIT) revert InsufficientAmount();
if (receiver == address(0)) revert InvalidAddress();
// IMPORTANT: Since all collateral tokens are enforced to be 8 decimals (same as sovaBTC),
// and treated as 1:1 with sovaBTC, we can pass amount directly to previewDeposit
// which expects asset-denominated units (8 decimals). The ERC-4626 math handles
// the conversion to 18-decimal shares internally.
shares = previewDeposit(amount);
if (shares == 0) revert ZeroShares();
// Transfer collateral directly to strategy
token.safeTransferFrom(msg.sender, strategy, amount);
// Mint shares to receiver
_mint(receiver, shares);
emit CollateralDeposited(msg.sender, token, amount, shares, receiver);
}
/**
* @notice Preview shares for collateral deposit
* @param token Address of the BTC collateral token
* @param amount Amount of collateral to deposit
* @return shares Amount of shares that would be minted
*/
function previewDepositCollateral(address token, uint256 amount) external view returns (uint256 shares) {
if (!IBtcVaultStrategy(strategy).isSupportedAsset(token)) return 0;
if (amount < MIN_DEPOSIT) return 0;
// - Calculates shares = (amount * totalSupply) / totalAssets
// - Where totalAssets comes from ReportedStrategy.balance() using current pricePerShare
// - This ensures preview matches actual minting logic and respects vault NAV
shares = previewDeposit(amount);
}
/*//////////////////////////////////////////////////////////////
ERC-4626 OVERRIDES
//////////////////////////////////////////////////////////////*/
/**
* @notice Standard ERC-4626 deposit is disabled
* @dev Use depositCollateral for all deposits including sovaBTC
*/
function deposit(uint256, address) public virtual override returns (uint256) {
revert StandardDepositDisabled();
}
/**
* @notice Standard ERC-4626 mint is disabled
* @dev Use depositCollateral for all deposits including sovaBTC
*/
function mint(uint256, address) public virtual override returns (uint256) {
revert StandardDepositDisabled();
}
// Note: withdraw and redeem are already restricted in ManagedWithdrawRWA parent class
// They require onlyStrategy modifier, so users cannot call them directly
}
"
},
"lib/solady/src/utils/SafeTransferLib.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeTransferLib.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @author Permit2 operations from (https://github.com/Uniswap/permit2/blob/main/src/libraries/Permit2Lib.sol)
///
/// @dev Note:
/// - For ETH transfers, please use `forceSafeTransferETH` for DoS protection.
library SafeTransferLib {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The ETH transfer has failed.
error ETHTransferFailed();
/// @dev The ERC20 `transferFrom` has failed.
error TransferFromFailed();
/// @dev The ERC20 `transfer` has failed.
error TransferFailed();
/// @dev The ERC20 `approve` has failed.
error ApproveFailed();
/// @dev The ERC20 `totalSupply` query has failed.
error TotalSupplyQueryFailed();
/// @dev The Permit2 operation has failed.
error Permit2Failed();
/// @dev The Permit2 amount must be less than `2**160 - 1`.
error Permit2AmountOverflow();
/// @dev The Permit2 approve operation has failed.
error Permit2ApproveFailed();
/// @dev The Permit2 lockdown operation has failed.
error Permit2LockdownFailed();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Suggested gas stipend for contract receiving ETH that disallows any storage writes.
uint256 internal constant GAS_STIPEND_NO_STORAGE_WRITES = 2300;
/// @dev Suggested gas stipend for contract receiving ETH to perform a few
/// storage reads and writes, but low enough to prevent griefing.
uint256 internal constant GAS_STIPEND_NO_GRIEF = 100000;
/// @dev The unique EIP-712 domain domain separator for the DAI token contract.
bytes32 internal constant DAI_DOMAIN_SEPARATOR =
0xdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7;
/// @dev The address for the WETH9 contract on Ethereum mainnet.
address internal constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
/// @dev The canonical Permit2 address.
/// [Github](https://github.com/Uniswap/permit2)
/// [Etherscan](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3)
address internal constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ETH OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
// If the ETH transfer MUST succeed with a reasonable gas budget, use the force variants.
//
// The regular variants:
// - Forwards all remaining gas to the target.
// - Reverts if the target reverts.
// - Reverts if the current contract has insufficient balance.
//
// The force variants:
// - Forwards with an optional gas stipend
// (defaults to `GAS_STIPEND_NO_GRIEF`, which is sufficient for most cases).
// - If the target reverts, or if the gas stipend is exhausted,
// creates a temporary contract to force send the ETH via `SELFDESTRUCT`.
// Future compatible with `SENDALL`: https://eips.ethereum.org/EIPS/eip-4758.
// - Reverts if the current contract has insufficient balance.
//
// The try variants:
// - Forwards with a mandatory gas stipend.
// - Instead of reverting, returns whether the transfer succeeded.
/// @dev Sends `amount` (in wei) ETH to `to`.
function safeTransferETH(address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gas(), to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Sends all the ETH in the current contract to `to`.
function safeTransferAllETH(address to) internal {
/// @solidity memory-safe-assembly
assembly {
// Transfer all the ETH and check if it succeeded or not.
if iszero(call(gas(), to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Force sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
function forceSafeTransferETH(address to, uint256 amount, uint256 gasStipend) internal {
/// @solidity memory-safe-assembly
assembly {
if lt(selfbalance(), amount) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
if iszero(call(gasStipend, to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends all the ETH in the current contract to `to`, with a `gasStipend`.
function forceSafeTransferAllETH(address to, uint256 gasStipend) internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gasStipend, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends `amount` (in wei) ETH to `to`, with `GAS_STIPEND_NO_GRIEF`.
function forceSafeTransferETH(address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
if lt(selfbalance(), amount) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
if iszero(call(GAS_STIPEND_NO_GRIEF, to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends all the ETH in the current contract to `to`, with `GAS_STIPEND_NO_GRIEF`.
function forceSafeTransferAllETH(address to) internal {
/// @solidity memory-safe-assembly
assembly {
// forgefmt: disable-next-item
if iszero(call(GAS_STIPEND_NO_GRIEF, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
function trySafeTransferETH(address to, uint256 amount, uint256 gasStipend)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
success := call(gasStipend, to, amount, codesize(), 0x00, codesize(), 0x00)
}
}
/// @dev Sends all the ETH in the current contract to `to`, with a `gasStipend`.
function trySafeTransferAllETH(address to, uint256 gasStipend)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
success := call(gasStipend, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERC20 OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
/// Reverts upon failure.
///
/// The `from` account must have at least `amount` approved for
/// the current contract to manage.
function safeTransferFrom(address token, address from, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, amount) // Store the `amount` argument.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x23b872dd000000000000000000000000) // `transferFrom(address,address,uint256)`.
let success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
///
/// The `from` account must have at least `amount` approved for the current contract to manage.
function trySafeTransferFrom(address token, address from, address to, uint256 amount)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, amount) // Store the `amount` argument.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x23b872dd000000000000000000000000) // `transferFrom(address,address,uint256)`.
success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
success := lt(or(iszero(extcodesize(token)), returndatasize()), success)
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends all of ERC20 `token` from `from` to `to`.
/// Reverts upon failure.
///
/// The `from` account must have their entire balance approved for the current contract to manage.
function safeTransferAllFrom(address token, address from, address to)
internal
returns (uint256 amount)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x70a08231000000000000000000000000) // `balanceOf(address)`.
// Read the balance, reverting upon failure.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x1c, 0x24, 0x60, 0x20)
)
) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
mstore(0x00, 0x23b872dd) // `transferFrom(address,address,uint256)`.
amount := mload(0x60) // The `amount` is already at 0x60. We'll need to return it.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends `amount` of ERC20 `token` from the current contract to `to`.
/// Reverts upon failure.
function safeTransfer(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0xa9059cbb000000000000000000000000) // `transfer(address,uint256)`.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sends all of ERC20 `token` from the current contract to `to`.
/// Reverts upon failure.
function safeTransferAll(address token, address to) internal returns (uint256 amount) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0x70a08231) // Store the function selector of `balanceOf(address)`.
mstore(0x20, address()) // Store the address of the current contract.
// Read the balance, reverting upon failure.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x1c, 0x24, 0x34, 0x20)
)
) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
mstore(0x14, to) // Store the `to` argument.
amount := mload(0x34) // The `amount` is already at 0x34. We'll need to return it.
mstore(0x00, 0xa9059cbb000000000000000000000000) // `transfer(address,uint256)`.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract.
/// Reverts upon failure.
function safeApprove(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x3e3f8f73) // `ApproveFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract.
/// If the initial attempt to approve fails, attempts to reset the approved amount to zero,
/// then retries the approval again (some tokens, e.g. USDT, requires this).
/// Reverts upon failure.
function safeApproveWithRetry(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
// Perform the approval, retrying upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x34, 0) // Store 0 for the `amount`.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
pop(call(gas(), token, 0, 0x10, 0x44, codesize(), 0x00)) // Reset the approval.
mstore(0x34, amount) // Store back the original `amount`.
// Retry the approval, reverting upon failure.
success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
// Check the `extcodesize` again just in case the token selfdestructs lol.
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x3e3f8f73) // `ApproveFailed()`.
revert(0x1c, 0x04)
}
}
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Returns the amount of ERC20 `token` owned by `account`.
/// Returns zero if the `token` does not exist.
function balanceOf(address token, address account) internal view returns (uint256 amount) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, account) // Store the `account` argument.
mstore(0x00, 0x70a08231000000000000000000000000) // `balanceOf(address)`.
amount :=
mul( // The arguments of `mul` are evaluated from right to left.
mload(0x20),
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x10, 0x24, 0x20, 0x20)
)
)
}
}
/// @dev Returns the total supply of the `token`.
/// Reverts if the token does not exist or does not implement `totalSupply()`.
function totalSupply(address token) internal view returns (uint256 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0x18160ddd) // `totalSupply()`.
if iszero(
and(gt(returndatasize(), 0x1f), staticcall(gas(), token, 0x1c, 0x04, 0x00, 0x20))
) {
mstore(0x00, 0x54cd9435) // `TotalSupplyQueryFailed()`.
revert(0x1c, 0x04)
}
result := mload(0x00)
}
}
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
/// If the initial attempt fails, try to use Permit2 to transfer the token.
/// Reverts upon failure.
///
/// The `from` account must have at least `amount` approved for the current contract to manage.
function safeTransferFrom2(address token, address from, address to, uint256 amount) internal {
if (!trySafeTransferFrom(token, from, to, amount)) {
permit2TransferFrom(token, from, to, amount);
}
}
/// @dev Sends `amount` of ERC20 `token` from `from` to `to` via Permit2.
/// Reverts upon failure.
function permit2TransferFrom(address token, address from, address to, uint256 amount)
internal
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40)
mstore(add(m, 0x74), shr(96, shl(96, token)))
mstore(add(m, 0x54), amount)
mstore(add(m, 0x34), to)
mstore(add(m, 0x20), shl(96, from))
// `transferFrom(address,address,uint160,address)`.
mstore(m, 0x36c78516000000000000000000000000)
let p := PERMIT2
let exists := eq(chainid(), 1)
if iszero(exists) { exists := iszero(iszero(extcodesize(p))) }
if iszero(
and(
call(gas(), p, 0, add(m, 0x10), 0x84, codesize(), 0x00),
lt(iszero(extcodesize(token)), exists) // Token has code and Permit2 exists.
)
) {
mstore(0x00, 0x7939f4248757f0fd) // `TransferFromFailed()` or `Permit2AmountOverflow()`.
revert(add(0x18, shl(2, iszero(iszero(shr(160, amount))))), 0x04)
}
}
}
/// @dev Permit a user to spend a given amount of
/// another user's tokens via native EIP-2612 permit if possible, falling
/// back to Permit2 if native permit fails or is not implemented on the token.
function permit2(
address token,
address owner,
address spender,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
for {} shl(96, xor(token, WETH9)) {} {
mstore(0x00, 0x3644e515) // `DOMAIN_SEPARATOR()`.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
lt(iszero(mload(0x00)), eq(returndatasize(), 0x20)), // Returns 1 non-zero word.
// Gas stipend to limit gas burn for tokens that don't refund gas when
// an non-existing function is called. 5K should be enough for a SLOAD.
staticcall(5000, token, 0x1c, 0x04, 0x00, 0x20)
)
) { break }
// After here, we can be sure that token is a contract.
let m := mload(0x40)
mstore(add(m, 0x34), spender)
mstore(add(m, 0x20), shl(96, owner))
mstore(add(m, 0x74), deadline)
if eq(mload(0x00), DAI_DOMAIN_SEPARATOR) {
mstore(0x14, owner)
mstore(0x00, 0x7ecebe00000000000000000000000000) // `nonces(address)`.
mstore(
add(m, 0x94),
lt(iszero(amount), staticcall(gas(), token, 0x10, 0x24, add(m, 0x54), 0x20))
)
mstore(m, 0x8fcbaf0c000000000000000000000000) // `IDAIPermit.permit`.
// `nonces` is already at `add(m, 0x54)`.
// `amount != 0` is already stored at `add(m, 0x94)`.
mstore(add(m, 0xb4), and(0xff, v))
mstore(add(m, 0xd4), r)
mstore(add(m, 0xf4), s)
success := call(gas(), token, 0, add(m, 0x10), 0x104, codesize(), 0x00)
break
}
mstore(m, 0xd505accf000000000000000000000000) // `IERC20Permit.permit`.
mstore(add(m, 0x54), amount)
mstore(add(m, 0x94), and(0xff, v))
mstore(add(m, 0xb4), r)
mstore(add(m, 0xd4), s)
success := call(gas(), token, 0, add(m, 0x10), 0xe4, codesize(), 0x00)
break
}
}
if (!success) simplePermit2(token, owner, spender, amount, deadline, v, r, s);
}
/// @dev Simple permit on the Permit2 contract.
function simplePermit2(
address token,
address owner,
address spender,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40)
mstore(m, 0x927da105) // `allowance(address,address,address)`.
{
let addressMask := shr(96, not(0))
mstore(add(m, 0x20), and(addressMask, owner))
mstore(add(m, 0x40), and(addressMask, token))
mstore(add(m, 0x60), and(addressMask, spender))
mstore(add(m, 0xc0), and(addressMask, spender))
}
let p := mul(PERMIT2, iszero(shr(160, amount)))
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x5f), // Returns 3 words: `amount`, `expiration`, `nonce`.
staticcall(gas(), p, add(m, 0x1c), 0x64, add(m, 0x60), 0x60)
)
) {
mstore(0x00, 0x6b836e6b8757f0fd) // `Permit2Failed()` or `Permit2AmountOverflow()`.
revert(add(0x18, shl(2, iszero(p))), 0x04)
}
mstore(m, 0x2b67b570) // `Permit2.permit` (PermitSingle variant).
// `owner` is already `add(m, 0x20)`.
// `token` is already at `add(m, 0x40)`.
mstore(add(m, 0x60), amount)
mstore(add(m, 0x80), 0xffffffffffff) // `expiration = type(uint48).max`.
// `nonce` is already at `add(m, 0xa0)`.
// `spender` is already at `add(m, 0xc0)`.
mstore(add(m, 0xe0), deadline)
mstore(add(m, 0x100), 0x100) // `signature` offset.
mstore(add(m, 0x120), 0x41) // `signature` length.
mstore(add(m, 0x140), r)
mstore(add(m, 0x160), s)
mstore(add(m, 0x180), shl(248, v))
if iszero( // Revert if token does not have code, or if the call fails.
mul(extcodesize(token), call(gas(), p, 0, add(m, 0x1c), 0x184, codesize(), 0x00))) {
mstore(0x00, 0x6b836e6b) // `Permit2Failed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Approves `spender` to spend `amount` of `token` for `address(this)`.
function permit2Approve(address token, address spender, uint160 amount, uint48 expiration)
internal
{
/// @solidity memory-safe-assembly
assembly {
let addressMask := shr(96, not(0))
let m := mload(0x40)
mstore(m, 0x87517c45) // `approve(address,address,uint160,uint48)`.
mstore(add(m, 0x20), and(addressMask, token))
mstore(add(m, 0x40), and(addressMask, spender))
mstore(add(m, 0x60), and(addressMask, amount))
mstore(add(m, 0x80), and(0xffffffffffff, expiration))
if iszero(call(gas(), PERMIT2, 0, add(m, 0x1c), 0xa0, codesize(), 0x00)) {
mstore(0x00, 0x324f14ae) // `Permit2ApproveFailed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Revokes an approval for `token` and `spender` for `address(this)`.
function permit2Lockdown(address token, address spender) internal {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40)
mstore(m, 0xcc53287f) // `Permit2.lockdown`.
mstore(add(m, 0x20), 0x20) // Offset of the `approvals`.
mstore(add(m, 0x40), 1) // `approvals.length`.
mstore(add(m, 0x60), shr(96, shl(96, token)))
mstore(add(m, 0x80), shr(96, shl(96, spender)))
if iszero(call(gas(), PERMIT2, 0, add(m, 0x1c), 0xa0, codesize(), 0x00)) {
mstore(0x00, 0x96b3de23) // `Permit2LockdownFailed()`.
revert(0x1c, 0x04)
}
}
}
}
"
},
"src/libraries/CollateralManagementLib.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
// Minimal interface for decimals check
interface IERC20Decimals {
function decimals() external view returns (uint8);
}
/**
* @title CollateralManagementLib
* @notice Library for managing multi-collateral operations in BtcVaultStrategy
*/
library CollateralManagementLib {
using SafeTransferLib for address;
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error AssetNotSupported();
error AssetAlreadySupported();
error InvalidDecimals();
error InsufficientLiquidity();
error InvalidAmount();
error InvalidAddress();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event CollateralAdded(address indexed token, uint8 decimals);
event CollateralRemoved(address indexed token);
event LiquidityAdded(uint256 amount);
event LiquidityRemoved(uint256 amount);
event CollateralWithdrawn(address indexed token, uint256 amount, address indexed to);
event CollateralDeposited(address indexed depositor, address indexed token, uint256 amount);
/*//////////////////////////////////////////////////////////////
COLLATERAL MANAGEMENT
//////////////////////////////////////////////////////////////*/
/**
* @notice Add a new supported collateral token
* @param token Address of the BTC collateral token
* @param supportedAssets Mapping of supported assets
* @param collateralTokens Array of collateral tokens
* @param requiredDecimals Required decimals for collateral
*/
function addCollateral(
address token,
mapping(address => bool) storage supportedAssets,
address[] storage collateralTokens,
uint8 requiredDecimals
) external {
if (token == address(0)) revert InvalidAddress();
if (supportedAssets[token]) revert AssetAlreadySupported();
// Verify the token has required decimals
try IERC20Decimals(token).decimals() returns (uint8 decimals) {
if (decimals != requiredDecimals) revert InvalidDecimals();
} catch {
revert InvalidDecimals();
}
supportedAssets[token] = true;
collateralTokens.push(token);
emit CollateralAdded(token, requiredDecimals);
}
/**
* @notice Remove a supported collateral token
* @param token Address of the collateral token to remove
* @param asset The main asset address (cannot be removed)
* @param supportedAssets Mapping of supported assets
* @param collateralTokens Array of collateral tokens
*/
function removeCollateral(
address token,
address asset,
mapping(address => bool) storage supportedAssets,
address[] storage collateralTokens
) external {
if (!supportedAssets[token]) revert AssetNotSupported();
if (token == asset) revert InvalidAddress();
supportedAssets[token] = false;
// Remove from array
for (uint256 i = 0; i < collateralTokens.length;) {
if (collateralTokens[i] == token) {
collateralTokens[i] = collateralTokens[collateralTokens.length - 1];
collateralTokens.pop();
break;
}
unchecked {
++i;
}
}
emit CollateralRemoved(token);
}
/**
* @notice Deposit collateral directly to the strategy
* @param token The collateral token to deposit
* @param amount The amount to deposit
* @param supportedAssets Mapping of supported assets
*/
function depositCollateral(address token, uint256 amount, mapping(address => bool) storage supportedAssets)
external
{
if (!supportedAssets[token]) revert AssetNotSupported();
if (amount == 0) revert InvalidAmount();
// Transfer collateral from sender to this contract
token.safeTransferFrom(msg.sender, address(this), amount);
emit CollateralDeposited(msg.sender, token, amount);
}
/**
* @notice Withdraw collateral to admin (emergency or rebalancing)
* @param token Collateral token to withdraw
* @param amount Amount to withdraw
* @param to Recipient address
* @param supportedAssets Mapping of supported assets
*/
function withdrawCollateral(
address token,
uint256 amount,
address to,
mapping(address => bool) storage supportedAssets
) external {
if (!supportedAssets[token]) revert AssetNotSupported();
if (amount == 0) revert InvalidAmount();
if (to == address(0)) revert InvalidAddress();
uint256 tokenBalance = IERC20(token).balanceOf(address(this));
if (amount > tokenBalance) revert InsufficientLiquidity();
token.safeTransfer(to, amount);
emit CollateralWithdrawn(token, amount, to);
}
/*//////////////////////////////////////////////////////////////
LIQUIDITY MANAGEMENT
//////////////////////////////////////////////////////////////*/
/**
* @notice Add sovaBTC for redemptions
* @param asset The asset to add liquidity for
* @param amount Amount to add
*/
function addLiquidity(address asset, uint256 amount) external {
if (amount == 0) revert InvalidAmount();
// Transfer asset from manager
asset.safeTransferFrom(msg.sender, address(this), amount);
emit LiquidityAdded(amount);
}
/**
* @notice Remove sovaBTC from strategy
* @param asset The asset to remove liquidity for
* @param amount Amount to remove
* @param to Address to send the asset
*/
function removeLiquidity(address asset, uint256 amount, address to) external {
if (amount == 0) revert InvalidAmount();
uint256 balance = IERC20(asset).balanceOf(address(this));
if (amount > balance) revert InsufficientLiquidity();
if (to == address(0)) revert InvalidAddress();
asset.safeTransfer(to, amount);
emit LiquidityRemoved(amount);
}
}
"
},
"src/libraries/CollateralViewLib.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;
import {IERC20} from "forge-std/interfaces/IERC20.sol";
/**
* @title CollateralViewLib
* @notice Library for view functions in BtcVaultStrategy
*/
library CollateralViewLib {
/**
* @notice Get total collateral assets value in sovaBTC terms (1:1 for all BTC variants)
* @dev This sums raw collateral balances without NAV adjustment
* @param collateralTokens Array of collateral token addresses
* @return Total value of all collateral in 8 decimal units
*/
function totalCollateralAssets(address[] storage collateralTokens) external view returns (uint256) {
uint256 total = 0;
// Sum all collateral balances (all have 1:1 value with sovaBTC)
for (uint256 i = 0; i < collateralTokens.length;) {
address token = collateralTokens[i];
uint256 tokenBalance = IERC20(token).balanceOf(address(this));
total += tokenBalance; // All BTC tokens are 8 decimals, 1:1 with sovaBTC
unchecked {
++i;
}
}
return total;
}
/**
* @notice Get balance of a specific collateral token
* @param token Address of the collateral token
* @param supportedAssets Mapping of supported assets
* @return Balance of the token held by strategy
*/
function collateralBalance(address token, mapping(address => bool) storage supportedAssets)
external
view
returns (uint256)
{
if (!supportedAssets[token]) return 0;
return IERC20(token).balanceOf(address(this));
}
/**
* @notice Get available sovaBTC balance for redemptions
* @param asset The asset address
* @return Current sovaBTC balance in the strategy
*/
function availableLiquidity(address asset) external view returns (uint256) {
return IERC20(asset).balanceOf(address(this));
}
}
"
},
"lib/solady/src/utils/ECDSA.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Gas optimized ECDSA wrapper.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/ECDSA.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/ECDSA.sol)
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol)
///
/// @dev Note:
/// - The recovery functions use the ecrecover precompile (0x1).
/// - As of Solady version 0.0.68, the `recover` variants will revert upon recovery failure.
/// This is for more safety by default.
/// Use the `tryRecover` variants if you need to get the zero address back
/// upon recovery failure instead.
/// - As of Solady version 0.0.134, all `bytes signature` variants accept both
/// regular 65-byte `(r, s, v)` and EIP-2098 `(r, vs)` short form signatures.
/// See: https://eips.ethereum.org/EIPS/eip-2098
/// This is for calldata efficiency on smart accounts prevalent on L2s.
///
/// WARNING! Do NOT directly use signatures as unique identifiers:
/// - The recovery operations do NOT check if a signature is non-malleable.
/// - Use a nonce in the digest to prevent replay attacks on the same contract.
/// - Use EIP-712 for the digest to prevent replay attacks across different chains and contracts.
/// EIP-712 also enables readable signing of typed data for better user safety.
/// - If you need a unique hash from a signature, please use the `canonicalHash` functions.
library ECDSA {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The order of the secp256k1 elliptic curve.
uint256 internal constant N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141;
/// @dev `N/2 + 1`. Used for checking the malleability of the signature.
uint256 private constant _HALF_N_PLUS_1 =
0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a1;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The signature is invalid.
error InvalidSignature();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* RECOVERY OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Recovers the signer's address from a message digest `hash`, and the `signature`.
function recover(bytes32 hash, bytes memory signature) internal view returns (address result) {
/// @solidity memory-safe-assembly
assembly {
for { let m := mload(0x40) } 1 {
mstore(0x00, 0x8baa579f) // `InvalidSignature()`.
revert(0x1c, 0x04)
} {
switch mload(signature)
case 64 {
let vs := mload(add(signature, 0x40))
mstore(0x20, add(shr(255, vs), 27)) // `v`.
mstore(0x60, shr(1, shl(1, vs))) // `s`.
}
case 65 {
mstore(0x20, byte(0, mload(add(signature, 0x60)))) // `v`.
mstore(0x60, mload(add(signature, 0x40))) // `s`.
}
default { continue }
mstore(0x00, hash)
mstore(0x40, mload(add(signature, 0x20))) // `r`.
result := mload(staticcall(gas(), 1, 0x00, 0x80, 0x01, 0x20))
mstore(0x60, 0) // Restore the zero slot.
mstore(0x40, m) // Restore the free memory pointer.
// `returndatasize()` will be `0x20` upon success, and `0x00` otherwise.
if returndatasize() { break }
}
}
}
/// @dev Recovers the signer's address from a message digest `hash`, and the `signature`.
function recoverCalldata(bytes32 hash, bytes calldata signature)
internal
view
returns (address result)
{
/// @solidity memory-safe-assembly
assembly {
for { let m := mload(0x40) } 1 {
mstore(0x00, 0x8baa579f) // `InvalidSignature()`.
revert(0x1c, 0x04)
} {
switch signature.length
case 64 {
let vs := calldataload(add(signature.offset, 0x20))
mstore(0x20, add(shr(255, vs), 27)) // `v`.
mstore(0x40, calldataload(signature.offset)) // `r`.
mstore(0x60, shr(1, shl(1, vs))) // `s`.
}
case 65 {
mstore(0x20, byte(0, calldataload(add(signature.offset, 0x40)))) // `v`.
calldatacopy(0x40, signature.offset, 0x40) // Copy `r` and `s`.
}
default { continue }
mstore(0x00, hash)
result := mload(staticcall(gas(), 1, 0x00, 0x80, 0x01, 0x20))
mstore(0x60, 0) // Restore the zero slot.
mstore(0x40, m) // Restore the free memory pointer.
// `returndatasize()` will be `0x20` upon success, and `0x00` otherwise.
if returndatasize() { break }
}
}
}
/// @dev Recovers the signer's address from a message digest `hash`,
/// and the EIP-2098 short form signature defined by `r` and `vs`.
function recover(bytes32 hash, bytes32 r, bytes32 vs) internal view returns (address result) {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x00, hash)
mstore(0x20, add(shr(255, vs), 27)) // `v`.
mstore(0x40, r)
mstore(0x60, shr(1, shl(1, vs))) // `s`.
result := mload(staticcall(gas(), 1, 0x00, 0x80, 0x01, 0x20))
// `returndatasize()` will be `0x20` upon success, and `0x00` otherwise.
if iszero(returndatasize()) {
mstore(0x00, 0x8baa579f) // `InvalidSignature()`.
revert(0x1c, 0x04)
}
mstore(0x60, 0) // Restore the zero slot.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Recovers the signer's address from a message digest `hash`,
/// and the signature defined by `v`, `r`, `s`.
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
internal
view
returns (address result)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x00, hash)
mstore(0x20, and(v, 0xff))
mstore(0x40, r)
mstore(0x60, s)
result := mload(staticcall(gas(), 1, 0x00, 0x80, 0x01, 0x20))
// `returndatasize()` will be `0x20` upon success, and `0x00` otherwise.
if iszero(returndatasize()) {
mstore(0x00, 0x8baa579f) // `InvalidSignature()`.
revert(0x1c, 0x04)
}
mstore(0x60, 0) // Restore the zero slot.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/*´:°•.°+.*•´.*:˚.
Submitted on: 2025-10-23 16:42:12
Comments
Log in to comment.
No comments yet.