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/integrations/curve/lending/CurveLendingMarketFactory.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {CurveStableswapOracle} from "src/integrations/curve/oracles/CurveStableswapOracle.sol";
import {CurveCryptoswapOracle} from "src/integrations/curve/oracles/CurveCryptoswapOracle.sol";
import {IStrategyWrapper} from "src/interfaces/IStrategyWrapper.sol";
import {IOracle} from "src/interfaces/IOracle.sol";
import {IRewardVault} from "src/interfaces/IRewardVault.sol";
import {IProtocolController} from "src/interfaces/IProtocolController.sol";
import {ILendingFactory} from "src/interfaces/ILendingFactory.sol";
/// @title Curve Lending Market Factory
/// @notice Creates a lending market for Curve-associated Stake DAO reward vaults on the given lending protocol
/// @author Stake DAO
/// @custom:contact contact@stakedao.org
contract CurveLendingMarketFactory is Ownable2Step {
/// @dev The address of the Stake DAO Staking v2 protocol controller
/// Used to check if the reward vault is genuine
IProtocolController private immutable PROTOCOL_CONTROLLER;
///////////////////////////////////////////////////////////////
// --- EVENTS & ERRORS
///////////////////////////////////////////////////////////////
event CollateralDeployed(address collateral);
event OracleDeployed(OracleType indexed oracleType, address indexed oracle, OracleParams oracleParams);
/// @dev Thrown when the given address is zero.
error AddressZero();
/// @dev Thrown when the reward vault is not registered in the protocol controller.
error InvalidRewardVault();
/// @dev Thrown when the reward vault is not a Curve reward vault.
error InvalidProtocolId();
/// @dev Thrown when the oracle type is invalid.
error InvalidOracleType();
/// @dev Thrown when the delegate call fails.
error MarketCreationFailed();
/// @dev Thrown when the collateral and the lending factory are not compatible.
error InvalidLendingProtocol();
/// @dev Thrown when the transfer of ownership fails.
error FailedToTransferOwnership();
enum OracleType {
UNKNOWN, // safeguard against using the default value
STABLESWAP,
CRYPTOSWAP
}
/// @dev The parameters for creating a Curve oracle (both stableswap and cryptoswap).
struct OracleParams {
address loanAsset;
address loanAssetFeed;
uint256 loanAssetFeedHeartbeat;
address[] priceFeeds; // For stableswap: pool asset feeds. For cryptoswap: token0→USD feeds
uint256[] priceFeedsHeartbeats;
}
/// @dev The parameters needed for the market creation
/// Both the IRM and the LLTV must be valid values pre-approved by the lending protocol.
struct MarketParams {
/// The Interest Rate Model that must be enabled for the market
address irm;
/// The pre-approved Liquidation Loan-To-Value
uint256 lltv;
/// The initial amount of collateral to supply to the market
uint256 initialSupply;
}
constructor(address _protocolController) Ownable(msg.sender) {
require(_protocolController != address(0), AddressZero());
PROTOCOL_CONTROLLER = IProtocolController(_protocolController);
}
///////////////////////////////////////////////////////////////
// --- MARKET CREATION
///////////////////////////////////////////////////////////////
/// @notice Creates a Stake DAO market on Curve with the specified oracle type
/// @dev The lending factory must be trusted!
/// The ownership of this contract is propagated to the lending factory
/// @param collateral The collateral to use for the market
/// @param curvePool The Curve pool to use for the market
/// @param oracleType The type of oracle to deploy (STABLESWAP =1 or CRYPTOSWAP =2)
/// @param oracleParams The parameters for the oracle (structure works for both types)
/// @param marketParams The parameters for the market
/// @param lendingFactory The factory to use for the market
function deploy(
IStrategyWrapper collateral,
address curvePool,
OracleType oracleType,
OracleParams calldata oracleParams,
MarketParams calldata marketParams,
ILendingFactory lendingFactory
) external onlyOwner returns (IStrategyWrapper, IOracle, bytes memory data) {
// 1. Check if the deployment is valid
IRewardVault rewardVault = collateral.REWARD_VAULT();
require(collateral.LENDING_PROTOCOL() == lendingFactory.protocol(), InvalidLendingProtocol());
require(PROTOCOL_CONTROLLER.vault(rewardVault.gauge()) == address(rewardVault), InvalidRewardVault());
require(rewardVault.PROTOCOL_ID() == bytes4(keccak256("CURVE")), InvalidProtocolId());
require(oracleType != OracleType.UNKNOWN, InvalidOracleType());
// 2. Deploy the oracle
uint256 protocolScalingExponent = lendingFactory.protocolScalingExponent();
IOracle oracle = _deployOracle(curvePool, oracleType, oracleParams, protocolScalingExponent);
// 3. Create the lending market
data = _createLendingMarket(collateral, oracle, oracleParams.loanAsset, lendingFactory, marketParams);
// 4. Transfer ownership of the collateral from this contract to the owner of this contract
(bool success,) = address(collateral).call(abi.encodeWithSignature("transferOwnership(address)", owner()));
require(success, FailedToTransferOwnership());
return (collateral, oracle, data);
}
///////////////////////////////////////////////////////////////
// --- INTERNAL FUNCTIONS
///////////////////////////////////////////////////////////////
function _deployOracle(
address curvePool,
OracleType oracleType,
OracleParams calldata oracleParams,
uint256 protocolScalingExponent
) internal returns (IOracle oracle) {
if (oracleType == OracleType.CRYPTOSWAP) {
oracle = new CurveCryptoswapOracle(
curvePool,
oracleParams.loanAsset,
oracleParams.loanAssetFeed,
oracleParams.loanAssetFeedHeartbeat,
oracleParams.priceFeeds,
oracleParams.priceFeedsHeartbeats,
protocolScalingExponent
);
} else if (oracleType == OracleType.STABLESWAP) {
oracle = new CurveStableswapOracle(
curvePool,
oracleParams.loanAsset,
oracleParams.loanAssetFeed,
oracleParams.loanAssetFeedHeartbeat,
oracleParams.priceFeeds,
oracleParams.priceFeedsHeartbeats,
protocolScalingExponent
);
} else {
revert InvalidOracleType();
}
emit OracleDeployed(oracleType, address(oracle), oracleParams);
return oracle;
}
function _createLendingMarket(
IStrategyWrapper collateral,
IOracle oracle,
address loanAsset,
ILendingFactory lendingFactory,
MarketParams calldata marketParams
) internal returns (bytes memory) {
(bool success, bytes memory data) = address(lendingFactory)
.delegatecall(
abi.encodeWithSelector(
lendingFactory.create.selector,
address(collateral),
loanAsset,
address(oracle),
marketParams.irm,
marketParams.lltv,
marketParams.initialSupply
)
);
require(success, MarketCreationFailed());
return data;
}
}
"
},
"node_modules/@openzeppelin/contracts/access/Ownable2Step.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/Ownable2Step.sol)
pragma solidity ^0.8.20;
import {Ownable} from "./Ownable.sol";
/**
* @dev Contract module which provides access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* This extension of the {Ownable} contract includes a two-step mechanism to transfer
* ownership, where the new owner must call {acceptOwnership} in order to replace the
* old one. This can help prevent common mistakes, such as transfers of ownership to
* incorrect accounts, or to contracts that are unable to interact with the
* permission system.
*
* The initial owner is specified at deployment time in the constructor for `Ownable`. This
* can later be changed with {transferOwnership} and {acceptOwnership}.
*
* This module is used through inheritance. It will make available all functions
* from parent (Ownable).
*/
abstract contract Ownable2Step is Ownable {
address private _pendingOwner;
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
/**
* @dev Returns the address of the pending owner.
*/
function pendingOwner() public view virtual returns (address) {
return _pendingOwner;
}
/**
* @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
* Can only be called by the current owner.
*
* Setting `newOwner` to the zero address is allowed; this can be used to cancel an initiated ownership transfer.
*/
function transferOwnership(address newOwner) public virtual override onlyOwner {
_pendingOwner = newOwner;
emit OwnershipTransferStarted(owner(), newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual override {
delete _pendingOwner;
super._transferOwnership(newOwner);
}
/**
* @dev The new owner accepts the ownership transfer.
*/
function acceptOwnership() public virtual {
address sender = _msgSender();
if (pendingOwner() != sender) {
revert OwnableUnauthorizedAccount(sender);
}
_transferOwnership(sender);
}
}
"
},
"src/integrations/curve/oracles/CurveStableswapOracle.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {BaseOracle} from "src/integrations/curve/oracles/BaseOracle.sol";
import {ICurveStableSwapPool} from "src/interfaces/ICurvePool.sol";
/**
* @title CurveStableswapOracle
* @notice Read-only price oracle that returns the value of one Curve
* **StableSwap** LP token expressed in an arbitrary **quote
* asset** (USDC, crvUSD, USDT, …).
*
* The oracle is intended for lending markets where Curve's LP token (base asset) is
* used as collateral and the loan asset (quote asset) is the unit in which debts are
* denominated.
*
* Supported pool families: **StableSwap** pools and **StableSwap-NG** pools
* that implement the `price_oracle()` method.
*
* @dev Pricing formula
* ----------------
* Price(Base / Quote) = min(price_oracle(i)) × get_virtual_price() ⎫ in coin0
* × Π price(hopᵢ) ⎬ coin0 → USD
* ÷ price(Quote / USD) ⎭ USD → Quote
*
* • The base asset is the LP token itself
* • Uses Curve's EMA price oracle for conservative pricing across all pool assets
* • `price_oracle(i)` returns the price of coin[i+1] relative to coin0 (18 decimals)
* • `get_virtual_price()` returns the LP token price in coin0 terms (18 decimals)
* • `token0ToUsdFeeds` is an ordered array of Chainlink feeds that, hop by hop,
* convert coin0 into USD. Each element consumes the output of the previous hop.
*
* Appreciating Token Adjustment
* -------------------------------------------
* In StableSwap‑NG pools where coin0 is an appreciating wrapper (wstETH, sDAI, etc.),
* `get_virtual_price()` already pulls the rate-adjusted balance from `_stored_rates`,
* so the LP price it returns is quoted in the underlying principal token (stETH, DAI…),
* not in the wrapper’s nominal units. When configuring the hop chain, make the first
* feed price that principal token; otherwise the wrapper appreciation would be applied twice.
*
* Flash-manipulation caveat
* -------------------------------------------
* This oracle leverages Curve's battle-tested EMA price oracle which provides
* protection against flash loan attacks through exponential moving average smoothing.
* The conservative minimum pricing across all pool assets prevents overvaluation
* during asset depegs. This oracle is intended for high-TVL, curated pools only.
* Conservative LLTV settings are still recommended for additional safety.
*
* Feed availability and adapter pattern
* ------------------------------------
* When coin0 ≠ quote asset, the oracle requires a hop-chain to convert coin0 to USD.
* Each hop in the chain can return prices in any denomination - the chain composition
* determines the final USD conversion.
*
* Read-Only Reentrancy Attack
* ------------------------------------------------------
* This oracle relies on Curve's `get_virtual_price()` function which is vulnerable
* to read-only reentrancy attacks when the pool contains native ETH or ERC-777 tokens.
* The attack occurs when the token is sent to a malicious contract that reenters
* `get_virtual_price()` while the pool state is inconsistent (balances not yet updated
* but LP supply already decreased).
*
* Mitigation:
* • Only use pools where `get_virtual_price()` is protected with a nonreentrant modifier
*
* References:
* • https://www.chainsecurity.com/blog/curve-lp-oracle-manipulation-post-mortem
* • https://www.chainsecurity.com/blog/heartbreaks-curve-lp-oracles
*
* Optional Quote Asset Feed
* ------------------------------------
* When coin0 = quote asset, no external feeds are required and the oracle
* provides direct pricing with optimal gas efficiency.
*
* Limitations – Pool Compatibility
* -------------------------------------------
* This oracle only supports pools that implement the `price_oracle()` method.
* Pools without this method are incompatible and will revert during deployment.
* The oracle automatically detects pool configuration and supports both:
* • 2-coin pools: `price_oracle()` (no arguments)
* • Multi-coin pools: `price_oracle(i)` (with coin index argument)
*
* Limitations – Layer 2 sequencer availability
* -------------------------------------------
* This oracle lacks sequencer uptime validation for Layer 2 networks. Chainlink
* feeds on L2s can become stale if the sequencer goes down. For L2 deployments,
* consider wrapping this oracle to include Sequencer Uptime Data Feed checks.
* https://docs.chain.link/data-feeds/l2-sequencer-feeds
*
* @author Stake DAO
* @custom:github @stake-dao
* @custom:contact contact@stakedao.org
*/
contract CurveStableswapOracle is BaseOracle {
/// @dev Define if the price_oracle() method takes no arguments
bool internal immutable NO_ARGUMENT;
/// @dev Define the number of coins in the pool
uint256 internal immutable N_COINS;
/// @dev Define the rate multiplier of coin0. Used to normalize price of appreciating tokens
uint256 internal immutable COINS0_RATE_MULTIPLIER;
///////////////////////////////////////////////////////////////
// --- ERRORS
///////////////////////////////////////////////////////////////
error PoolMustHaveAtLeast2Coins();
error PoolDoesNotSupportPriceOracle();
error PoolIncompatiblePriceOracleImplementation();
constructor(
address _curvePool,
address _quoteAsset,
address _quoteAssetFeed,
uint256 _quoteAssetFeedHeartbeat,
address[] memory _token0ToUsdFeeds,
uint256[] memory _token0ToUsdHeartbeats,
uint256 _scalingExponent
)
BaseOracle(
_curvePool,
_quoteAsset,
_quoteAssetFeed,
_quoteAssetFeedHeartbeat,
_token0ToUsdFeeds,
_token0ToUsdHeartbeats,
_scalingExponent
)
{
(N_COINS, NO_ARGUMENT) = _detectPoolConfiguration(_curvePool);
// dry-run the internal function to detect any anomalies
_getLpPriceInCoin0();
}
///////////////////////////////////////////////////////////////
// --- OVERRIDDEN FUNCTIONS
///////////////////////////////////////////////////////////////
/// @notice Gets the LP token price in coin0 terms using Curve's EMA price oracle
/// @dev The base asset is the Curve pool address
/// @return lpPrice The LP token price in coin0 terms (18 decimals)
function _getLpPriceInCoin0() internal view override returns (uint256) {
uint256 minPrice = 1e18;
// Get minimum price across all coins (excluding coin0)
for (uint256 i; i < N_COINS - 1; i++) {
uint256 _price = NO_ARGUMENT
? ICurveStableSwapPool(BASE_ASSET).price_oracle()
: ICurveStableSwapPool(BASE_ASSET).price_oracle(i);
if (_price < minPrice) minPrice = _price;
}
uint256 virtualPrice = ICurveStableSwapPool(BASE_ASSET).get_virtual_price();
// Multiply by virtual price to get LP price in coin0 terms
return Math.mulDiv(minPrice, virtualPrice, 1e18);
// (18 dec) └─ minPrice(18-dec) × virtualPrice(18-dec) / 1e18
}
///////////////////////////////////////////////////////////////
// --- INTERNAL FUNCTIONS
///////////////////////////////////////////////////////////////
/// @notice Detects the pool configuration by testing price_oracle() calls
/// @param pool The Curve pool address
/// @return nCoins Number of coins in the pool
/// @return noArgument Whether price_oracle() takes no arguments
function _detectPoolConfiguration(address pool) internal view returns (uint256 nCoins, bool noArgument) {
// Find N_COINS by calling coins(i) until it fails. This is the universal way to detect the number of coins in all pool
for (uint256 i; i <= 8; i++) {
try ICurveStableSwapPool(pool).coins(i) returns (
address
) {
// Coin exists, continue
}
catch {
require(i > 1, PoolMustHaveAtLeast2Coins());
nCoins = i;
break;
}
}
// Test price_oracle() signature
for (uint256 i; i < nCoins - 1; i++) {
try ICurveStableSwapPool(pool).price_oracle(i) returns (uint256 _price) {
require(_price > 0, InvalidPrice());
// Method takes argument, continue testing
} catch {
// Method doesn't take any arguments, verify it's a 2-coin pool
require(i == 0 && nCoins == 2, PoolIncompatiblePriceOracleImplementation());
// Test the no-argument version
try ICurveStableSwapPool(pool).price_oracle() returns (uint256 _price) {
require(_price > 0, InvalidPrice());
noArgument = true;
} catch {
revert PoolDoesNotSupportPriceOracle();
}
break;
}
}
}
}
/*
──────────────────────────────────────────────────────────────────────────────
EXAMPLES – how to build the **token0 → USD** feed chain
──────────────────────────────────────────────────────────────────────────────
Constructor signature
CurveStableswapOracle(
curvePool,
quoteAsset, // USDC in the examples below
quoteAssetFeed, // USDC/USD Chainlink feed (not required if coin0 = quote asset)
quoteAssetHeartbeat, // Heartbeat for quote asset feed (not required if quoteAssetFeed is not set)
token0ToUsdFeeds, // Array of feeds to convert coin0 to USD (not required if coin0 = quote asset)
token0ToUsdHeartbeats // Array of heartbeats for each feed (not required if token0ToUsdFeeds is not set)
scalingExponent // Base exponent used to scale the price. Protocol specific.
)
Each element `token0ToUsdFeeds[i]` converts the *output* of the previous hop into the
*input* of the next hop, until the value is finally expressed in **USD**. For
lending markets that borrow **USDC**, you then pass the USDC/USD feed via the
dedicated `quoteAssetFeed` parameter.
IMPORTANT: The oracle uses Curve's EMA price oracle for conservative pricing.
The minimum price across all pool assets (excluding coin0) is used to prevent
overvaluation during asset depegs.
---------------------------------------------------------------------
1. USDC / USDT **StableSwap** pool → coin0 = USDC
---------------------------------------------------------------------
• Address: 0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85
• Pool assets: USDC, USDT
• coin0 = USDC (= quote asset)
• No conversion needed: direct pricing
token0ToUsdFeeds = [] // Empty array
token0ToUsdHeartbeats = [] // Empty array
quoteAssetFeed = address(0) // No feed needed
quoteAssetHeartbeat = 0 // No heartbeat needed
The oracle fetches the LP price directly from Curve's price oracle and scales it.
---------------------------------------------------------------------
2. USDT / crvUSD **StableSwap** pool → coin0 = USDT
---------------------------------------------------------------------
• Address: 0x390f3595bCa2Df7d23783dFd126427CCeb997BF4
• Pool assets: USDT, crvUSD
• coin0 = USDT (≠ quote asset USDC)
• Need to convert USDT → USD → USDC
token0ToUsdFeeds = [USDT/USD feed]
token0ToUsdHeartbeats = [1 days]
quoteAssetFeed = USDC/USD feed
quoteAssetHeartbeat = 1 days
The oracle uses Curve's minimum price (USDT vs crvUSD) and converts USDT to USDC.
---------------------------------------------------------------------
3. wETH / frxETH **StableSwap-NG** pool → coin0 = wETH
---------------------------------------------------------------------
• Address: 0x9c3B46C0Ceb5B9e304FCd6D88Fc50f7DD24B31Bc
• Pool assets: wETH, frxETH
• coin0 = wETH (≠ quote asset USDC)
• Need to convert ETH → USD → USDC (1 wETH = 1 ETH)
token0ToUsdFeeds = [ETH/USD feed]
token0ToUsdHeartbeats = [1 days]
quoteAssetFeed = USDC/USD feed
quoteAssetHeartbeat = 1 days
The oracle uses Curve's minimum price (wETH vs frxETH) and converts wETH to USDC.
---------------------------------------------------------------------
4. wBTC / tBTC **StableSwap** pool → coin0 = wBTC
---------------------------------------------------------------------
• Address: 0xB7ECB2AA52AA64a717180E030241bC75Cd946726
• Pool assets: wBTC, tBTC
• coin0 = wBTC (≠ quote asset USDC)
• Need to convert wBTC → BTC → USD → USDC (because there is no wBTC/USD feed)
token0ToUsdFeeds = [wBTC/BTC feed, BTC/USD feed]
token0ToUsdHeartbeats = [1 days, 1 days]
quoteAssetFeed = USDC/USD feed
quoteAssetHeartbeat = 1 days
The oracle uses Curve's minimum price (wBTC vs tBTC) and converts wBTC to USDC
via the hop chain: wBTC → BTC → USD → USDC.
---------------------------------------------------------------------
5. sUSDS / USDT **StableSwap** pool → coin0 = sUSDS
---------------------------------------------------------------------
• Address: 0x00836Fe54625BE242BcFA286207795405ca4fD10
• Pool assets: sUSDS, USDT
• coin0 = sUSDS (≠ quote asset USDC)
• Need to convert USDS → USDC (because coin0 is an appreciating token, the first feed must price the principal token, not the wrapper token!)
token0ToUsdFeeds = [USDS/USD feed] // Principal token feed
token0ToUsdHeartbeats = [23 hours]
quoteAssetFeed = USDC/USD feed
quoteAssetHeartbeat = 1 days
The coin0 is an appreciating token, so the first feed must price the principal token, not the wrapper token!
The oracle uses Curve's minimum price (sUSDS vs USDT) and converts USDS to USDC via the hop chain: USDS → USDC.
*/
"
},
"src/integrations/curve/oracles/CurveCryptoswapOracle.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {BaseOracle} from "src/integrations/curve/oracles/BaseOracle.sol";
import {ICurveCryptoSwapPool} from "src/interfaces/ICurvePool.sol";
/**
* @title CurveCryptoswapOracle
* @notice Read-only price oracle that returns the value of one Curve
* **Crypto-swap** LP token (base asset) expressed in an arbitrary
* quote asset (USDC, crvUSD, USDT, …).
*
* The oracle is intended for lending markets where Curve's LP token (base asset) is
* used as collateral and the loan asset (quote asset) is the unit in which debts are
* denominated.
*
* Supported pool families: **Crypto Pool**, **TwoCrypto-NG**, and **TriCrypto-NG**
*
* @dev Pricing formula
* ----------------
* Price(Base / Quote) = lp_price() ⎫ in coin0
* × Π price(hopᵢ) ⎬ coin0 → USD
* ÷ price(Quote / USD) ⎭ USD → Quote
*
* • The base asset is the LP token itself
* • Curve documentation guarantees that `lp_price()` is denominated in
* the coin at index 0 of the pool.
* • `token0ToUsdFeeds` is an ordered array of Chainlink feeds that, hop
* by hop, convert coin0 into USD. Each element consumes the output
* of the previous hop.
*
* Flash-manipulation caveat
* -------------------------------------------
* This oracle implements conservative minimum pricing across all pool assets
* and is intended for high-TVL, curated pools only. While `lp_price()`
* could theoretically be manipulated, the multi-layer protections and economic
* barriers make such attacks practically infeasible for the target deployment.
* Conservative LLTV settings are still recommended for additional safety.
*
* Feed availability and adapter pattern
* ------------------------------------
* When coin0 ≠ quote asset, the oracle requires a hop-chain to convert coin0 to USD.
* Each hop in the chain can return prices in any denomination - the chain composition
* determines the final USD conversion.
*
* Optional Quote Asset Feed
* ------------------------------------
* When coin0 = quote asset, no external feeds are required and the oracle
* provides direct pricing with optimal gas efficiency.
*
* Limitations – Layer 2 sequencer availability
* -------------------------------------------
* This oracle lacks sequencer uptime validation for Layer 2 networks. Chainlink
* feeds on L2s can become stale if the sequencer goes down. For L2 deployments,
* consider wrapping this oracle to include Sequencer Uptime Data Feed checks.
* https://docs.chain.link/data-feeds/l2-sequencer-feeds
*
* @author Stake DAO
* @custom:github @stake-dao
* @custom:contact contact@stakedao.org
*/
contract CurveCryptoswapOracle is BaseOracle {
constructor(
address _curvePool,
address _quoteAsset,
address _quoteAssetFeed,
uint256 _quoteAssetFeedHeartbeat,
address[] memory _token0ToUsdFeeds,
uint256[] memory _token0ToUsdHeartbeats,
uint256 _scalingExponent
)
BaseOracle(
_curvePool,
_quoteAsset,
_quoteAssetFeed,
_quoteAssetFeedHeartbeat,
_token0ToUsdFeeds,
_token0ToUsdHeartbeats,
_scalingExponent
)
{
// dry-run the internal function to detect any anomalies
_getLpPriceInCoin0();
}
///////////////////////////////////////////////////////////////
// --- OVERRIDDEN FUNCTIONS
///////////////////////////////////////////////////////////////
/// @notice Gets the LP token price in coin0 terms
/// @dev The base asset is the Curve pool address
/// @return lpPrice The LP token price in coin0 terms (18 decimals)
function _getLpPriceInCoin0() internal view override returns (uint256) {
return ICurveCryptoSwapPool(BASE_ASSET).lp_price();
}
}
/*
──────────────────────────────────────────────────────────────────────────────
EXAMPLES – how to build the **coin0 → USD** feed chain
──────────────────────────────────────────────────────────────────────────────
Constructor signature
CurveCryptoswapOracle(
curvePool,
quoteAsset, // USDC in the examples below
quoteAssetFeed, // USDC/USD Chainlink feed (not required if coin0 = quote asset)
quoteAssetHeartbeat, // Heartbeat for quote asset feed (not required if quoteAssetFeed is not set)
token0ToUsdFeeds, // Array of feeds to convert coin0 to USD (not required if coin0 = quote asset)
token0ToUsdHeartbeats // Array of heartbeats for each feed (not required if token0ToUsdFeeds is not set),
scalingExponent // Base exponent used to scale the price. Protocol specific.
)
Each element `token0ToUsdFeeds[i]` converts the *output* of the previous hop into the
*input* of the next hop, until the value is finally expressed in **USD**. For
lending markets that borrow **USDC**, you then pass the USDC/USD feed via the
dedicated `quoteAssetFeed` parameter.
IMPORTANT: The oracle uses Curve's `lp_price()` for conservative pricing.
When coin0 = quote asset, no external feeds are required for optimal gas efficiency.
---------------------------------------------------------------------
1. TriCrypto-USDC pool → coin0 = USDC
---------------------------------------------------------------------
• Address: 0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B
• Pool assets: USDC, wBTC, ETH
• coin0 = USDC (= quote asset)
• No conversion needed: direct pricing
token0ToUsdFeeds = [] // Empty array
token0ToUsdHeartbeats = [] // Empty array
quoteAssetFeed = address(0) // No feed needed
The oracle fetches the LP price directly from Curve's `lp_price()` and scales it.
---------------------------------------------------------------------
2. TriCrypto-USDT pool (USDT / wBTC / WETH) → coin0 = USDT
---------------------------------------------------------------------
• Address: 0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4
• Pool assets: USDT, wBTC, WETH
• coin0 = USDT (≠ quote asset USDC)
• Need to convert USDT → USD → USDC
token0ToUsdFeeds = [USDT/USD feed]
token0ToUsdHeartbeats = [1 days]
quoteAssetFeed = USDC/USD feed
quoteAssetHeartbeat = 1 days
The USDT/USD feed is mandatory so the oracle reacts to any USDT de-peg.
---------------------------------------------------------------------
3. wETH / RSUP pool → coin0 = WETH
---------------------------------------------------------------------
• Pool assets: wETH, RSUP
• coin0 = wETH (≠ quote asset USDC)
• WETH is a **1:1 non-rebasing wrapper** around ETH
• Need to convert ETH → USD → USDC
token0ToUsdFeeds = [ETH/USD feed] // Safe to skip wETH/ETH hop (1:1)
token0ToUsdHeartbeats = [1 days]
quoteAssetFeed = USDC/USD feed
quoteAssetHeartbeat = 1 days
Because wETH unwraps 1:1 into ETH, a single ETH/USD feed to convert coin0 to USD is required.
---------------------------------------------------------------------
4. swETH / frxETH pool → coin0 = swETH
---------------------------------------------------------------------
• Pool assets: swETH, frxETH
• coin0 = swETH (≠ quote asset USDC)
• swETH is a liquid staking derivative of ETH
• Need to convert swETH → ETH → USD → USDC (because there is no swETH/USD feed)
token0ToUsdFeeds = [swETH/ETH feed, ETH/USD feed]
token0ToUsdHeartbeats = [1 days, 1 days]
quoteAssetFeed = USDC/USD feed
quoteAssetHeartbeat = 1 days
The oracle converts swETH to USDC via the hop chain: swETH → ETH → USD → USDC.
This handles the case where liquid staking derivatives don't have direct USD feeds.
*/
"
},
"src/interfaces/IStrategyWrapper.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import {IRewardVault} from "src/interfaces/IRewardVault.sol";
interface IStrategyWrapper is IERC20, IERC20Metadata {
function REWARD_VAULT() external view returns (IRewardVault);
function LENDING_PROTOCOL() external view returns (address);
// Deposit
function depositShares() external;
function depositShares(uint256 amount) external;
function depositAssets() external;
function depositAssets(uint256 amount) external;
// Withdraw
function withdraw() external;
function withdraw(uint256 amount) external;
// Claim main reward token (e.g. CRV)
function claim() external returns (uint256 amount);
function claimExtraRewards() external returns (uint256[] memory amounts);
function claimExtraRewards(address[] calldata tokens) external returns (uint256[] memory amounts);
// Liquidation
function claimLiquidation(address liquidator, address victim, uint256 liquidatedAmount) external;
/*──────────────────────────────────────────
VIEW HELPERS
──────────────────────────────────────────*/
function getPendingRewards(address user) external view returns (uint256 rewards);
function getPendingExtraRewards(address user) external view returns (uint256[] memory rewards);
function getPendingExtraRewards(address user, address token) external view returns (uint256 rewards);
function lendingMarketId() external view returns (bytes32);
// Owner
function initialize(bytes32 marketId) external;
}
"
},
"src/interfaces/IOracle.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
interface IOracle {
function BASE_ASSET() external view returns (address);
function QUOTE_ASSET() external view returns (address);
function ORACLE_SCALING_EXPONENT() external view returns (uint256);
function price() external view returns (uint256);
function decimals() external view returns (uint8);
}
"
},
"src/interfaces/IRewardVault.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {IAccountant} from "src/interfaces/IAccountant.sol";
/// @title IRewardVault
/// @notice Interface for the RewardVault contract
interface IRewardVault is IERC4626 {
function addRewardToken(address rewardsToken, address distributor) external;
function depositRewards(address _rewardsToken, uint128 _amount) external;
function deposit(uint256 assets, address receiver, address referrer) external returns (uint256 shares);
function deposit(address account, address receiver, uint256 assets, address referrer)
external
returns (uint256 shares);
function claim(address[] calldata tokens, address receiver) external returns (uint256[] memory amounts);
function claim(address account, address[] calldata tokens, address receiver)
external
returns (uint256[] memory amounts);
function getRewardsDistributor(address token) external view returns (address);
function getLastUpdateTime(address token) external view returns (uint32);
function getPeriodFinish(address token) external view returns (uint32);
function getRewardRate(address token) external view returns (uint128);
function getRewardPerTokenStored(address token) external view returns (uint128);
function getRewardPerTokenPaid(address token, address account) external view returns (uint128);
function getClaimable(address token, address account) external view returns (uint128);
function getRewardTokens() external view returns (address[] memory);
function lastTimeRewardApplicable(address token) external view returns (uint256);
function rewardPerToken(address token) external view returns (uint128);
function earned(address account, address token) external view returns (uint128);
function isRewardToken(address rewardToken) external view returns (bool);
function resumeVault() external;
function gauge() external view returns (address);
function ACCOUNTANT() external view returns (IAccountant);
function checkpoint(address account) external;
function PROTOCOL_ID() external view returns (bytes4);
}
"
},
"src/interfaces/IProtocolController.sol": {
"content": "/// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
interface IProtocolController {
function vault(address) external view returns (address);
function asset(address) external view returns (address);
function rewardReceiver(address) external view returns (address);
function allowed(address, address, bytes4 selector) external view returns (bool);
function permissionSetters(address) external view returns (bool);
function isRegistrar(address) external view returns (bool);
function locker(bytes4 protocolId) external view returns (address);
function gateway(bytes4 protocolId) external view returns (address);
function strategy(bytes4 protocolId) external view returns (address);
function allocator(bytes4 protocolId) external view returns (address);
function accountant(bytes4 protocolId) external view returns (address);
function feeReceiver(bytes4 protocolId) external view returns (address);
function factory(bytes4 protocolId) external view returns (address);
function isPaused(bytes4) external view returns (bool);
function isShutdown(address) external view returns (bool);
function registerVault(address _gauge, address _vault, address _asset, address _rewardReceiver, bytes4 _protocolId)
external;
function setValidAllocationTarget(address _gauge, address _target) external;
function removeValidAllocationTarget(address _gauge, address _target) external;
function isValidAllocationTarget(address _gauge, address _target) external view returns (bool);
function pause(bytes4 protocolId) external;
function unpause(bytes4 protocolId) external;
function shutdown(address _gauge) external;
function unshutdown(address _gauge) external;
function setPermissionSetter(address _setter, bool _allowed) external;
function setPermission(address _contract, address _caller, bytes4 _selector, bool _allowed) external;
}
"
},
"src/interfaces/ILendingFactory.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IStrategyWrapper} from "src/interfaces/IStrategyWrapper.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IOracle} from "src/interfaces/IOracle.sol";
import {Id} from "@shared/src/interfaces/IMorpho.sol";
interface ILendingFactory {
function protocol() external view returns (address);
function create(
IStrategyWrapper collateral,
IERC20Metadata loan,
IOracle oracle,
address irm,
uint256 lltv,
uint256 initialLoanSupply
) external returns (Id);
function protocolScalingExponent() external view returns (uint256);
}
"
},
"node_modules/@openzeppelin/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"node_modules/@openzeppelin/contracts/utils/math/Math.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/Math.sol)
pragma solidity ^0.8.20;
import {Panic} from "../Panic.sol";
import {SafeCast} from "./SafeCast.sol";
/**
* @dev Standard math utilities missing in the Solidity language.
*/
library Math {
enum Rounding {
Floor, // Toward negative infinity
Ceil, // Toward positive infinity
Trunc, // Toward zero
Expand // Away from zero
}
/**
* @dev Returns the addition of two unsigned integers, with an success flag (no overflow).
*/
function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
unchecked {
uint256 c = a + b;
if (c < a) return (false, 0);
return (true, c);
}
}
/**
* @dev Returns the subtraction of two unsigned integers, with an success flag (no overflow).
*/
function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
unchecked {
if (b > a) return (false, 0);
return (true, a - b);
}
}
/**
* @dev Returns the multiplication of two unsigned integers, with an success flag (no overflow).
*/
function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
unchecked {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
if (a == 0) return (true, 0);
uint256 c = a * b;
if (c / a != b) return (false, 0);
return (true, c);
}
}
/**
* @dev Returns the division of two unsigned integers, with a success flag (no division by zero).
*/
function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
unchecked {
if (b == 0) return (false, 0);
return (true, a / b);
}
}
/**
* @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).
*/
function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
unchecked {
if (b == 0) return (false, 0);
return (true, a % b);
}
}
/**
* @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.
*
* IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.
* However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute
* one branch when needed, making this function more expensive.
*/
function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {
unchecked {
// branchless ternary works because:
// b ^ (a ^ b) == a
// b ^ 0 == b
return b ^ ((a ^ b) * SafeCast.toUint(condition));
}
}
/**
* @dev Returns the largest of two numbers.
*/
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return ternary(a > b, a, b);
}
/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return ternary(a < b, a, b);
}
/**
* @dev Returns the average of two numbers. The result is rounded towards
* zero.
*/
function average(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b) / 2 can overflow.
return (a & b) + (a ^ b) / 2;
}
/**
* @dev Returns the ceiling of the division of two numbers.
*
* This differs from standard division with `/` in that it rounds towards infinity instead
* of rounding towards zero.
*/
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
if (b == 0) {
// Guarantee the same behavior as in a regular Solidity division.
Panic.panic(Panic.DIVISION_BY_ZERO);
}
// The following calculation ensures accurate ceiling division without overflow.
// Since a is non-zero, (a - 1) / b will not overflow.
// The largest possible result occurs when (a - 1) / b is type(uint256).max,
// but the largest value we can obtain is type(uint256).max - 1, which happens
// when a = type(uint256).max and b = 1.
unchecked {
return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);
}
}
/**
* @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or
* denominator == 0.
*
* Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by
* Uniswap Labs also under MIT license.
*/
function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use
// the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2²⁵⁶ + prod0.
uint256 prod0 = x * y; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(x, y, not(0))
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division.
if (prod1 == 0) {
// Solidity will revert if denominator == 0, unlike the div opcode on its own.
// The surrounding unchecked block does not change this fact.
// See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.
return prod0 / denominator;
}
// Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0.
if (denominator <= prod1) {
Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));
}
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0].
uint256 remainder;
assembly {
// Compute remainder using mulmod.
remainder := mulmod(x, y, denominator)
// Subtract 256 bit number from 512 bit number.
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator and compute largest power of two divisor of denominator.
// Always >= 1. See https://cs.stackexchange.com/q/138556/92363.
uint256 twos = denominator & (0 - denominator);
assembly {
// Divide denominator by twos.
denominator := div(denominator, twos)
// Divide [prod1 prod0] by twos.
prod0 := div(prod0, twos)
// Flip twos such that it is 2²⁵⁶ / twos. If twos is zero, then it becomes one.
twos := add(div(sub(0, twos), twos), 1)
}
// Shift in bits from prod1 into prod0.
prod0 |= prod1 * twos;
// Invert denominator mod 2²⁵⁶. Now that denominator is an odd number, it has an inverse modulo 2²⁵⁶ such
// that denominator * inv ≡ 1 mod 2²⁵⁶. Compute the inverse by starting with a seed that is correct for
// four bits. That is, denominator * inv ≡ 1 mod 2⁴.
uint256 inverse = (3 * denominator) ^ 2;
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also
// works in modular arithmetic, doubling the correct bits in each step.
inverse *= 2 - denominator * inverse; // inverse mod 2⁸
inverse *= 2 - denominator * inverse; // inverse mod 2¹⁶
inverse *= 2 - denominator * inverse; // inverse mod 2³²
inverse *= 2 - denominator * inverse; // inverse mod 2⁶⁴
inverse *= 2 - denominator * inverse; // inverse mod 2¹²⁸
inverse *= 2 - denominator * inverse; // inverse mod 2²⁵⁶
// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
// This will give us the correct result modulo 2²⁵⁶. Since the preconditions guarantee that the outcome is
// less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inverse;
return result;
}
}
/**
* @dev Calculates x * y / denominator with full precision, following the selected rounding direction.
*/
function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {
return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);
}
/**
* @dev Calculate the modular multiplicative inverse of a number in Z/nZ.
*
* If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.
* If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.
*
* If the input value is not inversible, 0 is returned.
*
* NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the
* inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.
*/
function invMod(uint256 a, uint256 n) internal pure returns (uint256) {
unchecked {
if (n == 0) return 0;
// The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version)
// Used to compute integers x and y such that: ax + ny = gcd(a, n).
// When the gcd is 1, then the inverse of a modulo n exists and it's x.
// ax + ny = 1
// ax = 1 + (-y)n
// ax ≡ 1 (mod n) # x is the inverse of a modulo n
// If the remainder is 0 the gcd is n right away.
uint256 remainder = a % n;
uint256 gcd = n;
// Therefore the initial coefficients are:
// ax + ny = gcd(a, n) = n
// 0a + 1n = n
int256 x = 0;
int256 y = 1;
while (remainder != 0) {
uint256 quotient = gcd / remainder;
(gcd, remainder) = (
// The old remainder is the next gcd to try.
remainder,
// Compute the next remainder.
// Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd
// where gcd is at most n (capped to type(uint256).max)
gcd - remainder * quotient
);
(x, y) = (
// Increment the coefficient of a.
y,
// Decrement the coefficient of n.
// Can overflow, but the result is casted to uint256 so that the
// next value of y is "wrapped around" to a value between 0 and n - 1.
x - y * int256(quotient)
);
}
if (gcd != 1) return 0; // No inverse exists.
return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.
}
}
/**
* @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.
*
* From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is
* prime, then `a**(p-1) ≡ 1 mod p`. As a consequence, we have `a * a**(p-2) ≡ 1 mod p`, which means that
* `a**(p-2)` is the modular multiplicative inverse of a in Fp.
*
* NOTE: this function does NOT check that `p` is a prime greater than `2`.
*/
function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {
unchecked {
return Math.modExp(a, p - 2, p);
}
}
/**
* @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)
*
* Requirements:
* - modulus can't be zero
* - underlying staticcall to precompile must succeed
*
* IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make
* sure the chain you're using it on supports the precompiled contract for modular exponentiation
* at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise,
* the underlying function will succeed given the lack of a revert, but the result may be incorrectly
* interpreted as 0.
*/
function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) {
(bool success, uint256 result) = tryModExp(b, e, m);
if (!success) {
Panic.panic(Panic.DIVISION_BY_ZERO);
}
return result;
}
/**
* @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).
* It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying
* to operate modulo 0 or if the underlying precompile reverted.
*
* IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain
* you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in
* https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack
* of a revert, but the result may be incorrectly interpreted as 0.
*/
function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {
if (m == 0) return (false, 0);
assembly ("memory-safe") {
let ptr := mload(0x40)
// | Offset | Content | Content (Hex) |
// |-----------|------------|--------------------------------------------------------------------|
// | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 |
// | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 |
// | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 |
// | 0x60:0x7f | value of b | 0x<.............................................................b> |
// | 0x80:0x9f | value of e | 0x<.............................................................e> |
// | 0xa0:0xbf | value of m | 0x<.............................................................m> |
mstore(ptr, 0x20)
mstore(add(ptr, 0x20), 0x20)
mstore(add(ptr, 0x40), 0x20)
mstore(add(ptr, 0x60), b)
mstore(add(ptr, 0x80), e)
mstore(add(ptr, 0xa0), m)
// Given the result < m, it's guaranteed to fit in 32 bytes,
// so we can use the memory scratch space located at offset 0.
success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20)
result := mload(0x00)
}
}
/**
* @dev Variant of {modExp} that supports inputs of arbitrary length.
*/
function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) {
(bool success, bytes memory result) = tryModExp(b, e, m);
if (!success) {
Panic.panic(Panic.DIVISION_BY_ZERO);
}
return result;
}
/**
* @dev Variant of {tryModExp} that supports inputs of arbitrary length.
*/
function tryModExp(
bytes memory b,
bytes memory e,
bytes memory m
) internal view returns (bool success, bytes memory result) {
if (_zeroBytes(m)) return (false, new bytes(0));
uint256 mLen = m.length;
// Encode call args in result and move the free memory pointer
result = abi.encodePacked(b.length, e.length, mLen, b, e, m);
assembly ("memory-safe") {
let dataPtr := add(result, 0x20)
// Write result on top of args to avoid allocating extra memory.
success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)
// Overwrite the length.
// result.length > returndatasize() is guaranteed because returndatasize() == m.length
mstore(result, mLen)
// Set the memory pointer after the returned data.
mstore(0x40, add(dataPtr, mLen))
}
}
/**
* @dev Returns whether the provided byte array is zero.
*/
function _zeroBytes(bytes memory byteArray) private pure returns (bool) {
for (uint256 i = 0; i < byteArray.length; ++i) {
if (byteArray[i] != 0) {
return false;
}
}
return true;
}
/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded
* towards zero.
*
* This method is based on Newton's method for computing square roots; the algorithm is restricted to only
* using integer operations.
*/
function sqrt(uint256 a) internal pure returns (uint256) {
unchecked {
// Take care of easy edge cases when a == 0 or a == 1
if (a <= 1) {
return a;
}
// In this function, we use Newton's method to get a root of `f(x) := x² - a`. It involves building a
// sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between
// the current value as `ε_n = | x_n - sqrt(a) |`.
//
// For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root
// of the target. (i.e. `2**(e-1) ≤ sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is
// bigger than any uint256.
/
Submitted on: 2025-11-03 12:45:26
Comments
Log in to comment.
No comments yet.