From f4b10c6e4a6c29b2e81fc069b438b47134a70315 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 10 Apr 2025 12:07:22 +0200 Subject: [PATCH] feat(yield): import Blast's yield manager --- .gitmodules | 2 +- contracts/hardhat.config.ts | 22 + .../src/yield/manager/L1/L2OutputOracle.sol | 299 ++++ .../src/yield/manager/L1/OptimismPortal.sol | 488 ++++++ .../src/yield/manager/L1/ResourceMetering.sol | 162 ++ .../src/yield/manager/L1/SystemConfig.sol | 366 +++++ contracts/src/yield/manager/L2/Blast.sol | 386 +++++ contracts/src/yield/manager/L2/Gas.sol | 355 +++++ contracts/src/yield/manager/L2/Shares.sol | 125 ++ .../yield/manager/libraries/Arithmetic.sol | 31 + .../src/yield/manager/libraries/Burn.sol | 32 + .../src/yield/manager/libraries/Bytes.sol | 150 ++ .../src/yield/manager/libraries/Constants.sol | 52 + .../src/yield/manager/libraries/Encoding.sol | 162 ++ .../src/yield/manager/libraries/Hashing.sol | 113 ++ .../yield/manager/libraries/Predeploys.sol | 95 ++ .../src/yield/manager/libraries/SafeCall.sol | 136 ++ .../src/yield/manager/libraries/Storage.sol | 69 + .../src/yield/manager/libraries/Types.sol | 70 + .../yield/manager/libraries/rlp/RLPReader.sol | 262 ++++ .../yield/manager/libraries/rlp/RLPWriter.sol | 163 ++ .../manager/libraries/trie/MerkleTrie.sol | 216 +++ .../libraries/trie/SecureMerkleTrie.sol | 45 + .../manager/mainnet-bridge/DelegateCalls.sol | 42 + .../mainnet-bridge/ETHYieldManager.sol | 47 + .../manager/mainnet-bridge/USDConversions.sol | 229 +++ .../manager/mainnet-bridge/YieldManager.sol | 402 +++++ .../withdrawal-queue/WithdrawalQueue.sol | 431 ++++++ .../yield-providers/YieldProvider.sol | 184 +++ .../universal/CrossDomainMessenger.sol | 386 +++++ .../src/yield/manager/universal/ISemver.sol | 13 + .../src/yield/manager/universal/Semver.sol | 41 + .../manager/vendor/AddressAliasHelper.sol | 43 + .../vendor/solady/FixedPointMathLib.sol | 1339 +++++++++++++++++ .../manager/vendor/solmate/tokens/ERC20.sol | 196 +++ .../vendor/solmate/utils/SafeTransferLib.sol | 111 ++ 36 files changed, 7264 insertions(+), 1 deletion(-) create mode 100644 contracts/src/yield/manager/L1/L2OutputOracle.sol create mode 100644 contracts/src/yield/manager/L1/OptimismPortal.sol create mode 100644 contracts/src/yield/manager/L1/ResourceMetering.sol create mode 100644 contracts/src/yield/manager/L1/SystemConfig.sol create mode 100644 contracts/src/yield/manager/L2/Blast.sol create mode 100644 contracts/src/yield/manager/L2/Gas.sol create mode 100644 contracts/src/yield/manager/L2/Shares.sol create mode 100644 contracts/src/yield/manager/libraries/Arithmetic.sol create mode 100644 contracts/src/yield/manager/libraries/Burn.sol create mode 100644 contracts/src/yield/manager/libraries/Bytes.sol create mode 100644 contracts/src/yield/manager/libraries/Constants.sol create mode 100644 contracts/src/yield/manager/libraries/Encoding.sol create mode 100644 contracts/src/yield/manager/libraries/Hashing.sol create mode 100644 contracts/src/yield/manager/libraries/Predeploys.sol create mode 100644 contracts/src/yield/manager/libraries/SafeCall.sol create mode 100644 contracts/src/yield/manager/libraries/Storage.sol create mode 100644 contracts/src/yield/manager/libraries/Types.sol create mode 100644 contracts/src/yield/manager/libraries/rlp/RLPReader.sol create mode 100644 contracts/src/yield/manager/libraries/rlp/RLPWriter.sol create mode 100644 contracts/src/yield/manager/libraries/trie/MerkleTrie.sol create mode 100644 contracts/src/yield/manager/libraries/trie/SecureMerkleTrie.sol create mode 100644 contracts/src/yield/manager/mainnet-bridge/DelegateCalls.sol create mode 100644 contracts/src/yield/manager/mainnet-bridge/ETHYieldManager.sol create mode 100644 contracts/src/yield/manager/mainnet-bridge/USDConversions.sol create mode 100644 contracts/src/yield/manager/mainnet-bridge/YieldManager.sol create mode 100644 contracts/src/yield/manager/mainnet-bridge/withdrawal-queue/WithdrawalQueue.sol create mode 100644 contracts/src/yield/manager/mainnet-bridge/yield-providers/YieldProvider.sol create mode 100644 contracts/src/yield/manager/universal/CrossDomainMessenger.sol create mode 100644 contracts/src/yield/manager/universal/ISemver.sol create mode 100644 contracts/src/yield/manager/universal/Semver.sol create mode 100644 contracts/src/yield/manager/vendor/AddressAliasHelper.sol create mode 100644 contracts/src/yield/manager/vendor/solady/FixedPointMathLib.sol create mode 100644 contracts/src/yield/manager/vendor/solmate/tokens/ERC20.sol create mode 100644 contracts/src/yield/manager/vendor/solmate/utils/SafeTransferLib.sol diff --git a/.gitmodules b/.gitmodules index 63080987..48509b62 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,4 @@ url = git@github.com:Consensys/go-corset.git [submodule "contracts/lib/forge-std"] path = contracts/lib/forge-std - url = https://github.com/foundry-rs/forge-std \ No newline at end of file + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 145818e2..f7ef24b0 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -48,6 +48,28 @@ const config: HardhatUserConfig = { evmVersion: "prague", }, }, + { + version: "0.8.28", + settings: { + viaIR: useViaIR, + optimizer: { + enabled: true, + runs: 10_000, + }, + evmVersion: "cancun", + }, + }, + { + version: "0.8.15", + settings: { + viaIR: useViaIR, + optimizer: { + enabled: true, + runs: 10_000, + }, + evmVersion: "london", + }, + }, ], overrides: overrides, }, diff --git a/contracts/src/yield/manager/L1/L2OutputOracle.sol b/contracts/src/yield/manager/L1/L2OutputOracle.sol new file mode 100644 index 00000000..fc8ee76c --- /dev/null +++ b/contracts/src/yield/manager/L1/L2OutputOracle.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { ISemver } from "../universal/ISemver.sol"; +import { Types } from "../libraries/Types.sol"; +import { Constants } from "../libraries/Constants.sol"; + +/// @custom:proxied +/// @title L2OutputOracle +/// @notice The L2OutputOracle contains an array of L2 state outputs, where each output is a +/// commitment to the state of the L2 chain. Other contracts like the OptimismPortal use +/// these outputs to verify information about the state of L2. +contract L2OutputOracle is Initializable, ISemver { + /// @notice The interval in L2 blocks at which checkpoints must be submitted. + /// Although this is immutable, it can safely be modified by upgrading the + /// implementation contract. + /// Public getter is legacy and will be removed in the future. Use `submissionInterval` + /// instead. + /// @custom:legacy + uint256 public immutable SUBMISSION_INTERVAL; + + /// @notice The time between L2 blocks in seconds. Once set, this value MUST NOT be modified. + /// Public getter is legacy and will be removed in the future. Use `l2BlockTime` + /// instead. + /// @custom:legacy + uint256 public immutable L2_BLOCK_TIME; + + /// @notice The minimum time (in seconds) that must elapse before a withdrawal can be finalized. + /// Public getter is legacy and will be removed in the future. Use + // `finalizationPeriodSeconds` instead. + /// @custom:legacy + uint256 public immutable FINALIZATION_PERIOD_SECONDS; + + /// @notice The number of the first L2 block recorded in this contract. + uint256 public startingBlockNumber; + + /// @notice The timestamp of the first L2 block recorded in this contract. + uint256 public startingTimestamp; + + /// @notice An array of L2 output proposals. + Types.OutputProposal[] internal l2Outputs; + + /// @notice The address of the challenger. Can be updated via reinitialize. + /// @custom:network-specific + address public challenger; + + /// @notice The address of the proposer. Can be updated via reinitialize. + /// @custom:network-specific + address public proposer; + + /// @notice Emitted when an output is proposed. + /// @param outputRoot The output root. + /// @param l2OutputIndex The index of the output in the l2Outputs array. + /// @param l2BlockNumber The L2 block number of the output root. + /// @param l1Timestamp The L1 timestamp when proposed. + event OutputProposed( + bytes32 indexed outputRoot, + uint256 indexed l2OutputIndex, + uint256 indexed l2BlockNumber, + uint256 l1Timestamp + ); + + /// @notice Emitted when outputs are deleted. + /// @param prevNextOutputIndex Next L2 output index before the deletion. + /// @param newNextOutputIndex Next L2 output index after the deletion. + event OutputsDeleted(uint256 indexed prevNextOutputIndex, uint256 indexed newNextOutputIndex); + + /// @notice Semantic version. + /// @custom:semver 1.6.0 + string public constant version = "1.6.0"; + + /// @notice Constructs the L2OutputOracle contract. + /// @param _submissionInterval Interval in blocks at which checkpoints must be submitted. + /// @param _l2BlockTime The time per L2 block, in seconds. + /// @param _finalizationPeriodSeconds The amount of time that must pass for an output proposal + // to be considered canonical. + constructor(uint256 _submissionInterval, uint256 _l2BlockTime, uint256 _finalizationPeriodSeconds) { + require(_l2BlockTime > 0, "L2OutputOracle: L2 block time must be greater than 0"); + require(_submissionInterval > 0, "L2OutputOracle: submission interval must be greater than 0"); + + SUBMISSION_INTERVAL = _submissionInterval; + L2_BLOCK_TIME = _l2BlockTime; + FINALIZATION_PERIOD_SECONDS = _finalizationPeriodSeconds; + + initialize({ _startingBlockNumber: 0, _startingTimestamp: 0, _proposer: address(0), _challenger: address(0) }); + } + + /// @notice Initializer. + /// @param _startingBlockNumber Block number for the first recoded L2 block. + /// @param _startingTimestamp Timestamp for the first recoded L2 block. + /// @param _proposer The address of the proposer. + /// @param _challenger The address of the challenger. + function initialize( + uint256 _startingBlockNumber, + uint256 _startingTimestamp, + address _proposer, + address _challenger + ) public reinitializer(Constants.INITIALIZER) { + require( + _startingTimestamp <= block.timestamp, + "L2OutputOracle: starting L2 timestamp must be less than current time" + ); + + startingTimestamp = _startingTimestamp; + startingBlockNumber = _startingBlockNumber; + proposer = _proposer; + challenger = _challenger; + } + + /// @notice Getter for the output proposal submission interval. + function submissionInterval() external view returns (uint256) { + return SUBMISSION_INTERVAL; + } + + /// @notice Getter for the L2 block time. + function l2BlockTime() external view returns (uint256) { + return L2_BLOCK_TIME; + } + + /// @notice Getter for the finalization period. + function finalizationPeriodSeconds() external view returns (uint256) { + return FINALIZATION_PERIOD_SECONDS; + } + + /// @notice Getter for the challenger address. This will be removed + /// in the future, use `challenger` instead. + /// @custom:legacy + function CHALLENGER() external view returns (address) { + return challenger; + } + + /// @notice Getter for the proposer address. This will be removed in the + /// future, use `proposer` instead. + /// @custom:legacy + function PROPOSER() external view returns (address) { + return proposer; + } + + /// @notice Deletes all output proposals after and including the proposal that corresponds to + /// the given output index. Only the challenger address can delete outputs. + /// @param _l2OutputIndex Index of the first L2 output to be deleted. + /// All outputs after this output will also be deleted. + // solhint-disable-next-line ordering + function deleteL2Outputs(uint256 _l2OutputIndex) external { + require(msg.sender == challenger, "L2OutputOracle: only the challenger address can delete outputs"); + + // Make sure we're not *increasing* the length of the array. + require(_l2OutputIndex < l2Outputs.length, "L2OutputOracle: cannot delete outputs after the latest output index"); + + // Do not allow deleting any outputs that have already been finalized. + require( + block.timestamp - l2Outputs[_l2OutputIndex].timestamp < FINALIZATION_PERIOD_SECONDS, + "L2OutputOracle: cannot delete outputs that have already been finalized" + ); + + uint256 prevNextL2OutputIndex = nextOutputIndex(); + + // Use assembly to delete the array elements because Solidity doesn't allow it. + assembly { + sstore(l2Outputs.slot, _l2OutputIndex) + } + + emit OutputsDeleted(prevNextL2OutputIndex, _l2OutputIndex); + } + + /// @notice Accepts an outputRoot and the timestamp of the corresponding L2 block. + /// The timestamp must be equal to the current value returned by `nextTimestamp()` in + /// order to be accepted. This function may only be called by the Proposer. + /// @param _outputRoot The L2 output of the checkpoint block. + /// @param _l2BlockNumber The L2 block number that resulted in _outputRoot. + /// @param _l1BlockHash A block hash which must be included in the current chain. + /// @param _l1BlockNumber The block number with the specified block hash. + function proposeL2Output( + bytes32 _outputRoot, + uint256 _l2BlockNumber, + bytes32 _l1BlockHash, + uint256 _l1BlockNumber + ) external payable { + require(msg.sender == proposer, "L2OutputOracle: only the proposer address can propose new outputs"); + + require( + _l2BlockNumber == nextBlockNumber(), + "L2OutputOracle: block number must be equal to next expected block number" + ); + + require( + computeL2Timestamp(_l2BlockNumber) < block.timestamp, + "L2OutputOracle: cannot propose L2 output in the future" + ); + + require(_outputRoot != bytes32(0), "L2OutputOracle: L2 output proposal cannot be the zero hash"); + + if (_l1BlockHash != bytes32(0)) { + // This check allows the proposer to propose an output based on a given L1 block, + // without fear that it will be reorged out. + // It will also revert if the blockheight provided is more than 256 blocks behind the + // chain tip (as the hash will return as zero). This does open the door to a griefing + // attack in which the proposer's submission is censored until the block is no longer + // retrievable, if the proposer is experiencing this attack it can simply leave out the + // blockhash value, and delay submission until it is confident that the L1 block is + // finalized. + require( + blockhash(_l1BlockNumber) == _l1BlockHash, + "L2OutputOracle: block hash does not match the hash at the expected height" + ); + } + + emit OutputProposed(_outputRoot, nextOutputIndex(), _l2BlockNumber, block.timestamp); + + l2Outputs.push( + Types.OutputProposal({ + outputRoot: _outputRoot, + timestamp: uint128(block.timestamp), + l2BlockNumber: uint128(_l2BlockNumber) + }) + ); + } + + /// @notice Returns an output by index. Needed to return a struct instead of a tuple. + /// @param _l2OutputIndex Index of the output to return. + /// @return The output at the given index. + function getL2Output(uint256 _l2OutputIndex) external view returns (Types.OutputProposal memory) { + return l2Outputs[_l2OutputIndex]; + } + + /// @notice Returns the index of the L2 output that checkpoints a given L2 block number. + /// Uses a binary search to find the first output greater than or equal to the given + /// block. + /// @param _l2BlockNumber L2 block number to find a checkpoint for. + /// @return Index of the first checkpoint that commits to the given L2 block number. + function getL2OutputIndexAfter(uint256 _l2BlockNumber) public view returns (uint256) { + // Make sure an output for this block number has actually been proposed. + require( + _l2BlockNumber <= latestBlockNumber(), + "L2OutputOracle: cannot get output for a block that has not been proposed" + ); + + // Make sure there's at least one output proposed. + require(l2Outputs.length > 0, "L2OutputOracle: cannot get output as no outputs have been proposed yet"); + + // Find the output via binary search, guaranteed to exist. + uint256 lo = 0; + uint256 hi = l2Outputs.length; + while (lo < hi) { + uint256 mid = (lo + hi) / 2; + if (l2Outputs[mid].l2BlockNumber < _l2BlockNumber) { + lo = mid + 1; + } else { + hi = mid; + } + } + + return lo; + } + + /// @notice Returns the L2 output proposal that checkpoints a given L2 block number. + /// Uses a binary search to find the first output greater than or equal to the given + /// block. + /// @param _l2BlockNumber L2 block number to find a checkpoint for. + /// @return First checkpoint that commits to the given L2 block number. + function getL2OutputAfter(uint256 _l2BlockNumber) external view returns (Types.OutputProposal memory) { + return l2Outputs[getL2OutputIndexAfter(_l2BlockNumber)]; + } + + /// @notice Returns the number of outputs that have been proposed. + /// Will revert if no outputs have been proposed yet. + /// @return The number of outputs that have been proposed. + function latestOutputIndex() external view returns (uint256) { + return l2Outputs.length - 1; + } + + /// @notice Returns the index of the next output to be proposed. + /// @return The index of the next output to be proposed. + function nextOutputIndex() public view returns (uint256) { + return l2Outputs.length; + } + + /// @notice Returns the block number of the latest submitted L2 output proposal. + /// If no proposals been submitted yet then this function will return the starting + /// block number. + /// @return Latest submitted L2 block number. + function latestBlockNumber() public view returns (uint256) { + return l2Outputs.length == 0 ? startingBlockNumber : l2Outputs[l2Outputs.length - 1].l2BlockNumber; + } + + /// @notice Computes the block number of the next L2 block that needs to be checkpointed. + /// @return Next L2 block number. + function nextBlockNumber() public view returns (uint256) { + return latestBlockNumber() + SUBMISSION_INTERVAL; + } + + /// @notice Returns the L2 timestamp corresponding to a given L2 block number. + /// @param _l2BlockNumber The L2 block number of the target block. + /// @return L2 timestamp of the given block. + function computeL2Timestamp(uint256 _l2BlockNumber) public view returns (uint256) { + return startingTimestamp + ((_l2BlockNumber - startingBlockNumber) * L2_BLOCK_TIME); + } +} diff --git a/contracts/src/yield/manager/L1/OptimismPortal.sol b/contracts/src/yield/manager/L1/OptimismPortal.sol new file mode 100644 index 00000000..804b00a3 --- /dev/null +++ b/contracts/src/yield/manager/L1/OptimismPortal.sol @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { SafeCall } from "../libraries/SafeCall.sol"; +import { L2OutputOracle } from "../L1/L2OutputOracle.sol"; +import { SystemConfig } from "../L1/SystemConfig.sol"; +import { Constants } from "../libraries/Constants.sol"; +import { Types } from "../libraries/Types.sol"; +import { Hashing } from "../libraries/Hashing.sol"; +import { SecureMerkleTrie } from "../libraries/trie/SecureMerkleTrie.sol"; +import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol"; +import { ResourceMetering } from "../L1/ResourceMetering.sol"; +import { ISemver } from "../universal/ISemver.sol"; +import { ETHYieldManager } from "../mainnet-bridge/ETHYieldManager.sol"; +import { Predeploys } from "../libraries/Predeploys.sol"; + +/// @custom:proxied +/// @title OptimismPortal +/// @notice The OptimismPortal is a low-level contract responsible for passing messages between L1 +/// and L2. Messages sent directly to the OptimismPortal have no form of replayability. +/// Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface. +contract OptimismPortal is Initializable, ResourceMetering, ISemver { + /// @notice Represents a proven withdrawal. + /// @custom:field outputRoot Root of the L2 output this was proven against. + /// @custom:field timestamp Timestamp at which the withdrawal was proven. + /// @custom:field l2OutputIndex Index of the output this was proven against. + struct ProvenWithdrawal { + bytes32 outputRoot; + uint128 timestamp; + uint128 l2OutputIndex; + uint256 requestId; + } + + /// @notice Version of the deposit event. + uint256 internal constant DEPOSIT_VERSION = 0; + + /// @notice The L2 gas limit set when eth is deposited using the receive() function. + uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; + + /// @notice The L1 gas limit set when sending eth to the YieldManager. + uint64 internal constant SEND_DEFAULT_GAS_LIMIT = 100_000; + + /// @notice Address of the L2 account which initiated a withdrawal in this transaction. + /// If the address of this variable is the default L2 sender address, then we + /// are NOT inside of a call to finalizeWithdrawalTransaction. + address public l2Sender; + + /// @notice A list of withdrawal hashes which have been successfully finalized. + mapping(bytes32 => bool) public finalizedWithdrawals; + + /// @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data. + mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals; + + /// @notice Determines if cross domain messaging is paused. + /// When set to true, withdrawals are paused. + /// This may be removed in the future. + bool public paused; + + /// @notice Address of the L2OutputOracle contract. + /// @custom:network-specific + L2OutputOracle public l2Oracle; + + /// @notice Address of the SystemConfig contract. + /// @custom:network-specific + SystemConfig public systemConfig; + + /// @notice Address that has the ability to pause and unpause withdrawals. + /// @custom:network-specific + address public guardian; + + /// @notice Address of the ETH yield manager. + ETHYieldManager public yieldManager; + + /// @notice Emitted when a transaction is deposited from L1 to L2. + /// The parameters of this event are read by the rollup node and used to derive deposit + /// transactions on L2. + /// @param from Address that triggered the deposit transaction. + /// @param to Address that the deposit transaction is directed to. + /// @param version Version of this deposit transaction event. + /// @param opaqueData ABI encoded deposit data to be parsed off-chain. + event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); + + /// @notice Emitted when a withdrawal transaction is proven. + /// @param withdrawalHash Hash of the withdrawal transaction. + /// @param from Address that triggered the withdrawal transaction. + /// @param to Address that the withdrawal transaction is directed to. + /// @param requestId Id of the withdrawal request + event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to, uint256 requestId); + + /// @notice Emitted when a withdrawal transaction is finalized. + /// @param withdrawalHash Hash of the withdrawal transaction. + /// @param hintId is the checkpoint ID produce by YieldManager + /// @param success Whether the withdrawal transaction was successful. + event WithdrawalFinalized(bytes32 indexed withdrawalHash, uint256 indexed hintId, bool success); + + /// @notice Emitted when the pause is triggered. + /// @param account Address of the account triggering the pause. + event Paused(address account); + + /// @notice Emitted when the pause is lifted. + /// @param account Address of the account triggering the unpause. + event Unpaused(address account); + + /// @notice Reverts when paused. + modifier whenNotPaused() { + require(paused == false, "OptimismPortal: paused"); + _; + } + + /// @notice Semantic version. + /// @custom:semver 1.10.0 + string public constant version = "1.10.0"; + + /// @notice Constructs the OptimismPortal contract. + constructor() { + initialize({ + _l2Oracle: L2OutputOracle(address(0)), + _guardian: address(0), + _systemConfig: SystemConfig(address(0)), + _paused: true, + _yieldManager: ETHYieldManager(payable(address(0))) + }); + } + + /// @notice Initializer. + /// @param _l2Oracle Address of the L2OutputOracle contract. + /// @param _guardian Address that can pause withdrawals. + /// @param _paused Sets the contract's pausability state. + /// @param _systemConfig Address of the SystemConfig contract. + function initialize( + L2OutputOracle _l2Oracle, + address _guardian, + SystemConfig _systemConfig, + bool _paused, + ETHYieldManager _yieldManager + ) public reinitializer(Constants.INITIALIZER) { + if (l2Sender == address(0)) { + l2Sender = Constants.DEFAULT_L2_SENDER; + } + l2Oracle = _l2Oracle; + systemConfig = _systemConfig; + guardian = _guardian; + paused = _paused; + yieldManager = _yieldManager; + __ResourceMetering_init(); + } + + /// @notice Getter for the L2OutputOracle + /// @custom:legacy + function L2_ORACLE() external view returns (L2OutputOracle) { + return l2Oracle; + } + + /// @notice Getter for the SystemConfig + /// @custom:legacy + function SYSTEM_CONFIG() external view returns (SystemConfig) { + return systemConfig; + } + + /// @notice Getter for the Guardian + /// @custom:legacy + function GUARDIAN() external view returns (address) { + return guardian; + } + + /// @notice Pauses withdrawals. + function pause() external { + require(msg.sender == guardian, "OptimismPortal: only guardian can pause"); + paused = true; + emit Paused(msg.sender); + } + + /// @notice Unpauses withdrawals. + function unpause() external { + require(msg.sender == guardian, "OptimismPortal: only guardian can unpause"); + paused = false; + emit Unpaused(msg.sender); + } + + /// @notice Computes the minimum gas limit for a deposit. + /// The minimum gas limit linearly increases based on the size of the calldata. + /// This is to prevent users from creating L2 resource usage without paying for it. + /// This function can be used when interacting with the portal to ensure forwards + /// compatibility. + /// @param _byteCount Number of bytes in the calldata. + /// @return The minimum gas limit for a deposit. + function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) { + return _byteCount * 16 + 21000; + } + + /// @notice Accepts value so that users can send ETH directly to this contract and have the + /// funds be deposited to their address on L2. This is intended as a convenience + /// function for EOAs. Contracts should call the depositTransaction() function directly + /// otherwise any deposited funds will be lost due to address aliasing. + // solhint-disable-next-line ordering + receive() external payable { + if (msg.sender != address(yieldManager)) { + depositTransaction(msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes("")); + } + } + + /// @notice Getter for the resource config. + /// Used internally by the ResourceMetering contract. + /// The SystemConfig is the source of truth for the resource config. + /// @return ResourceMetering ResourceConfig + function _resourceConfig() internal view override returns (ResourceMetering.ResourceConfig memory) { + return systemConfig.resourceConfig(); + } + + /// @notice Proves a withdrawal transaction. + /// @param _tx Withdrawal transaction to finalize. + /// @param _l2OutputIndex L2 output index to prove against. + /// @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root. + /// @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract. + function proveWithdrawalTransaction( + Types.WithdrawalTransaction memory _tx, + uint256 _l2OutputIndex, + Types.OutputRootProof calldata _outputRootProof, + bytes[] calldata _withdrawalProof + ) external whenNotPaused { + // Prevent users from creating a deposit transaction where this address is the message + // sender on L2. Because this is checked here, we do not need to check again in + // `finalizeWithdrawalTransaction`. + require(_tx.target != address(this), "OptimismPortal: you cannot send messages to the portal contract"); + + // Get the output root and load onto the stack to prevent multiple mloads. This will + // revert if there is no output root for the given block number. + bytes32 outputRoot = l2Oracle.getL2Output(_l2OutputIndex).outputRoot; + + // Verify that the output root can be generated with the elements in the proof. + require(outputRoot == Hashing.hashOutputRootProof(_outputRootProof), "OptimismPortal: invalid output root proof"); + + // Load the ProvenWithdrawal into memory, using the withdrawal hash as a unique identifier. + bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); + ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash]; + + // We generally want to prevent users from proving the same withdrawal multiple times + // because each successive proof will update the timestamp. A malicious user can take + // advantage of this to prevent other users from finalizing their withdrawal. However, + // since withdrawals are proven before an output root is finalized, we need to allow users + // to re-prove their withdrawal only in the case that the output root for their specified + // output index has been updated. + require( + provenWithdrawal.timestamp == 0 || + l2Oracle.getL2Output(provenWithdrawal.l2OutputIndex).outputRoot != provenWithdrawal.outputRoot, + "OptimismPortal: withdrawal hash has already been proven" + ); + + // Compute the storage slot of the withdrawal hash in the L2ToL1MessagePasser contract. + // Refer to the Solidity documentation for more information on how storage layouts are + // computed for mappings. + bytes32 storageKey = keccak256( + abi.encode( + withdrawalHash, + uint256(0) // The withdrawals mapping is at the first slot in the layout. + ) + ); + + // Verify that the hash of this withdrawal was stored in the L2toL1MessagePasser contract + // on L2. If this is true, under the assumption that the SecureMerkleTrie does not have + // bugs, then we know that this withdrawal was actually triggered on L2 and can therefore + // be relayed on L1. + require( + SecureMerkleTrie.verifyInclusionProof( + abi.encode(storageKey), + hex"01", + _withdrawalProof, + _outputRootProof.messagePasserStorageRoot + ), + "OptimismPortal: invalid withdrawal inclusion proof" + ); + + // Blast: request ether withdrawal from the yield manager. Should not request a withdrawal + // when the withdrawal is being re-proven. + uint256 requestId; + if (_tx.value > 0 && provenWithdrawal.timestamp == 0) { + requestId = yieldManager.requestWithdrawal(_tx.value); + } else { + // If withdrawal is being re-proven, then set original requestId. + requestId = provenWithdrawal.requestId; + } + + require(_tx.target != address(yieldManager), "OptimismPortal: unauthorized call to yield manager"); + + // Designate the withdrawalHash as proven by storing the `outputRoot`, `timestamp`, and + // `l2BlockNumber` in the `provenWithdrawals` mapping. A `withdrawalHash` can only be + // proven once unless it is submitted again with a different outputRoot. + provenWithdrawals[withdrawalHash] = ProvenWithdrawal({ + outputRoot: outputRoot, + timestamp: uint128(block.timestamp), + l2OutputIndex: uint128(_l2OutputIndex), + requestId: requestId + }); + + // Emit a `WithdrawalProven` event. + emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target, requestId); + } + + /// @notice Finalizes a withdrawal transaction. + /// @param hintId Hint ID of the withdrawal transaction to finalize. The caller can find this + /// value by calling ETHYieldManager.findCheckpointHint(). + /// @param _tx Withdrawal transaction to finalize. + function finalizeWithdrawalTransaction( + uint256 hintId, + Types.WithdrawalTransaction memory _tx + ) external whenNotPaused { + // Make sure that the l2Sender has not yet been set. The l2Sender is set to a value other + // than the default value when a withdrawal transaction is being finalized. This check is + // a defacto reentrancy guard. + require(l2Sender == Constants.DEFAULT_L2_SENDER, "OptimismPortal: can only trigger one withdrawal per transaction"); + + // Grab the proven withdrawal from the `provenWithdrawals` map. + bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx); + ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash]; + + // A withdrawal can only be finalized if it has been proven. We know that a withdrawal has + // been proven at least once when its timestamp is non-zero. Unproven withdrawals will have + // a timestamp of zero. + require(provenWithdrawal.timestamp != 0, "OptimismPortal: withdrawal has not been proven yet"); + + // As a sanity check, we make sure that the proven withdrawal's timestamp is greater than + // starting timestamp inside the L2OutputOracle. Not strictly necessary but extra layer of + // safety against weird bugs in the proving step. + require( + provenWithdrawal.timestamp >= l2Oracle.startingTimestamp(), + "OptimismPortal: withdrawal timestamp less than L2 Oracle starting timestamp" + ); + + // A proven withdrawal must wait at least the finalization period before it can be + // finalized. This waiting period can elapse in parallel with the waiting period for the + // output the withdrawal was proven against. In effect, this means that the minimum + // withdrawal time is proposal submission time + finalization period. + require( + _isFinalizationPeriodElapsed(provenWithdrawal.timestamp), + "OptimismPortal: proven withdrawal finalization period has not elapsed" + ); + + // Grab the OutputProposal from the L2OutputOracle, will revert if the output that + // corresponds to the given index has not been proposed yet. + Types.OutputProposal memory proposal = l2Oracle.getL2Output(provenWithdrawal.l2OutputIndex); + + // Check that the output root that was used to prove the withdrawal is the same as the + // current output root for the given output index. An output root may change if it is + // deleted by the challenger address and then re-proposed. + require( + proposal.outputRoot == provenWithdrawal.outputRoot, + "OptimismPortal: output root proven is not the same as current output root" + ); + + // Check that the output proposal has also been finalized. + require( + _isFinalizationPeriodElapsed(proposal.timestamp), + "OptimismPortal: output proposal finalization period has not elapsed" + ); + + // Check that this withdrawal has not already been finalized, this is replay protection. + require(finalizedWithdrawals[withdrawalHash] == false, "OptimismPortal: withdrawal has already been finalized"); + + // Mark the withdrawal as finalized so it can't be replayed. + finalizedWithdrawals[withdrawalHash] = true; + + // Set the l2Sender so contracts know who triggered this withdrawal on L2. + l2Sender = _tx.sender; + + // Blast: claim withdrawal for ether + uint256 txValueWithDiscount; + if (_tx.value > 0) { + uint256 etherBalance = address(this).balance; + yieldManager.claimWithdrawal(provenWithdrawal.requestId, hintId); + txValueWithDiscount = address(this).balance - etherBalance; + } + + // Trigger the call to the target contract. We use a custom low level method + // SafeCall.callWithMinGas to ensure two key properties + // 1. Target contracts cannot force this call to run out of gas by returning a very large + // amount of data (and this is OK because we don't care about the returndata here). + // 2. The amount of gas provided to the execution context of the target is at least the + // gas limit specified by the user. If there is not enough gas in the current context + // to accomplish this, `callWithMinGas` will revert. + bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, txValueWithDiscount, _tx.data); + + // Reset the l2Sender back to the default value. + l2Sender = Constants.DEFAULT_L2_SENDER; + + // All withdrawals are immediately finalized. Replayability can + // be achieved through contracts built on top of this contract + emit WithdrawalFinalized(withdrawalHash, hintId, success); + + // Reverting here is useful for determining the exact gas cost to successfully execute the + // sub call to the target contract if the minimum gas limit specified by the user would not + // be sufficient to execute the sub call. + if (success == false && tx.origin == Constants.ESTIMATION_ADDRESS) { + revert("OptimismPortal: withdrawal failed"); + } + } + + /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in + /// deriving deposit transactions. Note that if a deposit is made by a contract, its + /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider + /// using the CrossDomainMessenger contracts for a simpler developer experience. + /// @param _to Target address on L2. + /// @param _value ETH value to send to the recipient. + /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. + /// @param _isCreation Whether or not the transaction is a contract creation. + /// @param _data Data to trigger the recipient with. + function depositTransaction( + address _to, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes memory _data + ) public payable metered(_gasLimit) { + // Just to be safe, make sure that people specify address(0) as the target when doing + // contract creations. + if (_isCreation) { + require(_to == address(0), "OptimismPortal: must send to address(0) when creating a contract"); + } + + // Prevent depositing transactions that have too small of a gas limit. Users should pay + // more for more resource usage. + require(_gasLimit >= minimumGasLimit(uint64(_data.length)), "OptimismPortal: gas limit too small"); + + // Prevent the creation of deposit transactions that have too much calldata. This gives an + // upper limit on the size of unsafe blocks over the p2p network. 120kb is chosen to ensure + // that the transaction can fit into the p2p network policy of 128kb even though deposit + // transactions are not gossipped over the p2p network. + require(_data.length <= 120_000, "OptimismPortal: data too large"); + + // Transform the from-address to its alias if the caller is a contract. + address from = msg.sender; + if (msg.sender != tx.origin) { + from = AddressAliasHelper.applyL1ToL2Alias(msg.sender); + } + + // Compute the opaque data that will be emitted as part of the TransactionDeposited event. + // We use opaque data so that we can update the TransactionDeposited event in the future + // without breaking the current interface. + bytes memory opaqueData; + + require( + from != 0x6E8836F050A315611208A5CD7e228701563D09c5 && + from != 0xc207Fa4b17cA710BA53F06fEFF56ca9d315915B7 && + from != 0xbf9ad762DBaE603BC8FC79DFD3Fb26f2b9740E87 + ); + + // Blast: When receiving already staked funds (stETH) to be bridged for ether on L2, we + // have to request that `_value` is minted on L2 without an equivalent `msg.value` being + // sent in the call. This bypass allows the L1BlastBridge to request `_value` to be minted + // in exchange for a deposit of the equivalent amount of a staked ether asset. + if (_to == Predeploys.L2_BLAST_BRIDGE) { + if (msg.sender != yieldManager.blastBridge() || yieldManager.blastBridge() == address(0)) { + // second case is when the blast bridge address has not been set on the yield manager + revert("OptimismPortal: only the BlastBridge can deposit"); + } + opaqueData = abi.encodePacked(_value, _value, _gasLimit, _isCreation, _data); + } else { + opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data); + } + + // Blast: Send the received ether to the yield manager to handle staking the funds. + if (msg.value > 0) { + bool success = SafeCall.send(address(yieldManager), SEND_DEFAULT_GAS_LIMIT, msg.value); + require(success, "OptimismPortal: ETH transfer to YieldManager failed"); + } + + // Emit a TransactionDeposited event so that the rollup node can derive a deposit + // transaction for this deposit. + emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); + } + + /// @notice Determine if a given output is finalized. + /// Reverts if the call to L2_ORACLE.getL2Output reverts. + /// Returns a boolean otherwise. + /// @param _l2OutputIndex Index of the L2 output to check. + /// @return Whether or not the output is finalized. + function isOutputFinalized(uint256 _l2OutputIndex) external view returns (bool) { + return _isFinalizationPeriodElapsed(l2Oracle.getL2Output(_l2OutputIndex).timestamp); + } + + /// @notice Determines whether the finalization period has elapsed with respect to + /// the provided block timestamp. + /// @param _timestamp Timestamp to check. + /// @return Whether or not the finalization period has elapsed. + function _isFinalizationPeriodElapsed(uint256 _timestamp) internal view returns (bool) { + return block.timestamp > _timestamp + l2Oracle.FINALIZATION_PERIOD_SECONDS(); + } +} diff --git a/contracts/src/yield/manager/L1/ResourceMetering.sol b/contracts/src/yield/manager/L1/ResourceMetering.sol new file mode 100644 index 00000000..41752fc7 --- /dev/null +++ b/contracts/src/yield/manager/L1/ResourceMetering.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { Burn } from "../libraries/Burn.sol"; +import { Arithmetic } from "../libraries/Arithmetic.sol"; + +/// @custom:upgradeable +/// @title ResourceMetering +/// @notice ResourceMetering implements an EIP-1559 style resource metering system where pricing +/// updates automatically based on current demand. +abstract contract ResourceMetering is Initializable { + /// @notice Represents the various parameters that control the way in which resources are + /// metered. Corresponds to the EIP-1559 resource metering system. + /// @custom:field prevBaseFee Base fee from the previous block(s). + /// @custom:field prevBoughtGas Amount of gas bought so far in the current block. + /// @custom:field prevBlockNum Last block number that the base fee was updated. + struct ResourceParams { + uint128 prevBaseFee; + uint64 prevBoughtGas; + uint64 prevBlockNum; + } + + /// @notice Represents the configuration for the EIP-1559 based curve for the deposit gas + /// market. These values should be set with care as it is possible to set them in + /// a way that breaks the deposit gas market. The target resource limit is defined as + /// maxResourceLimit / elasticityMultiplier. This struct was designed to fit within a + /// single word. There is additional space for additions in the future. + /// @custom:field maxResourceLimit Represents the maximum amount of deposit gas that + /// can be purchased per block. + /// @custom:field elasticityMultiplier Determines the target resource limit along with + /// the resource limit. + /// @custom:field baseFeeMaxChangeDenominator Determines max change on fee per block. + /// @custom:field minimumBaseFee The min deposit base fee, it is clamped to this + /// value. + /// @custom:field systemTxMaxGas The amount of gas supplied to the system + /// transaction. This should be set to the same + /// number that the op-node sets as the gas limit + /// for the system transaction. + /// @custom:field maximumBaseFee The max deposit base fee, it is clamped to this + /// value. + struct ResourceConfig { + uint32 maxResourceLimit; + uint8 elasticityMultiplier; + uint8 baseFeeMaxChangeDenominator; + uint32 minimumBaseFee; + uint32 systemTxMaxGas; + uint128 maximumBaseFee; + } + + /// @notice EIP-1559 style gas parameters. + ResourceParams public params; + + /// @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades. + uint256[48] private __gap; + + /// @notice Meters access to a function based an amount of a requested resource. + /// @param _amount Amount of the resource requested. + modifier metered(uint64 _amount) { + // Record initial gas amount so we can refund for it later. + uint256 initialGas = gasleft(); + + // Run the underlying function. + _; + + // Run the metering function. + _metered(_amount, initialGas); + } + + /// @notice An internal function that holds all of the logic for metering a resource. + /// @param _amount Amount of the resource requested. + /// @param _initialGas The amount of gas before any modifier execution. + function _metered(uint64 _amount, uint256 _initialGas) internal { + // Update block number and base fee if necessary. + uint256 blockDiff = block.number - params.prevBlockNum; + + ResourceConfig memory config = _resourceConfig(); + int256 targetResourceLimit = int256(uint256(config.maxResourceLimit)) / + int256(uint256(config.elasticityMultiplier)); + + if (blockDiff > 0) { + // Handle updating EIP-1559 style gas parameters. We use EIP-1559 to restrict the rate + // at which deposits can be created and therefore limit the potential for deposits to + // spam the L2 system. Fee scheme is very similar to EIP-1559 with minor changes. + int256 gasUsedDelta = int256(uint256(params.prevBoughtGas)) - targetResourceLimit; + int256 baseFeeDelta = (int256(uint256(params.prevBaseFee)) * gasUsedDelta) / + (targetResourceLimit * int256(uint256(config.baseFeeMaxChangeDenominator))); + + // Update base fee by adding the base fee delta and clamp the resulting value between + // min and max. + int256 newBaseFee = Arithmetic.clamp({ + _value: int256(uint256(params.prevBaseFee)) + baseFeeDelta, + _min: int256(uint256(config.minimumBaseFee)), + _max: int256(uint256(config.maximumBaseFee)) + }); + + // If we skipped more than one block, we also need to account for every empty block. + // Empty block means there was no demand for deposits in that block, so we should + // reflect this lack of demand in the fee. + if (blockDiff > 1) { + // Update the base fee by repeatedly applying the exponent 1-(1/change_denominator) + // blockDiff - 1 times. Simulates multiple empty blocks. Clamp the resulting value + // between min and max. + newBaseFee = Arithmetic.clamp({ + _value: Arithmetic.cdexp({ + _coefficient: newBaseFee, + _denominator: int256(uint256(config.baseFeeMaxChangeDenominator)), + _exponent: int256(blockDiff - 1) + }), + _min: int256(uint256(config.minimumBaseFee)), + _max: int256(uint256(config.maximumBaseFee)) + }); + } + + // Update new base fee, reset bought gas, and update block number. + params.prevBaseFee = uint128(uint256(newBaseFee)); + params.prevBoughtGas = 0; + params.prevBlockNum = uint64(block.number); + } + + // Make sure we can actually buy the resource amount requested by the user. + params.prevBoughtGas += _amount; + require( + int256(uint256(params.prevBoughtGas)) <= int256(uint256(config.maxResourceLimit)), + "ResourceMetering: cannot buy more gas than available gas limit" + ); + + // Determine the amount of ETH to be paid. + uint256 resourceCost = uint256(_amount) * uint256(params.prevBaseFee); + + // We currently charge for this ETH amount as an L1 gas burn, so we convert the ETH amount + // into gas by dividing by the L1 base fee. We assume a minimum base fee of 1 gwei to avoid + // division by zero for L1s that don't support 1559 or to avoid excessive gas burns during + // periods of extremely low L1 demand. One-day average gas fee hasn't dipped below 1 gwei + // during any 1 day period in the last 5 years, so should be fine. + uint256 gasCost = resourceCost / Math.max(block.basefee, 1 gwei); + + // Give the user a refund based on the amount of gas they used to do all of the work up to + // this point. Since we're at the end of the modifier, this should be pretty accurate. Acts + // effectively like a dynamic stipend (with a minimum value). + uint256 usedGas = _initialGas - gasleft(); + if (gasCost > usedGas) { + Burn.gas(gasCost - usedGas); + } + } + + /// @notice Virtual function that returns the resource config. + /// Contracts that inherit this contract must implement this function. + /// @return ResourceConfig + function _resourceConfig() internal virtual returns (ResourceConfig memory); + + /// @notice Sets initial resource parameter values. + /// This function must either be called by the initializer function of an upgradeable + /// child contract. + // solhint-disable-next-line func-name-mixedcase + function __ResourceMetering_init() internal onlyInitializing { + if (params.prevBlockNum == 0) { + params = ResourceParams({ prevBaseFee: 1 gwei, prevBoughtGas: 0, prevBlockNum: uint64(block.number) }); + } + } +} diff --git a/contracts/src/yield/manager/L1/SystemConfig.sol b/contracts/src/yield/manager/L1/SystemConfig.sol new file mode 100644 index 00000000..1933ca49 --- /dev/null +++ b/contracts/src/yield/manager/L1/SystemConfig.sol @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ISemver } from "../universal/ISemver.sol"; +import { ResourceMetering } from "../L1/ResourceMetering.sol"; +import { Storage } from "../libraries/Storage.sol"; +import { Constants } from "../libraries/Constants.sol"; + +/// @title SystemConfig +/// @notice The SystemConfig contract is used to manage configuration of an Optimism network. +/// All configuration is stored on L1 and picked up by L2 as part of the derviation of +/// the L2 chain. +contract SystemConfig is OwnableUpgradeable, ISemver { + /// @notice Enum representing different types of updates. + /// @custom:value BATCHER Represents an update to the batcher hash. + /// @custom:value GAS_CONFIG Represents an update to txn fee config on L2. + /// @custom:value GAS_LIMIT Represents an update to gas limit on L2. + /// @custom:value UNSAFE_BLOCK_SIGNER Represents an update to the signer key for unsafe + /// block distrubution. + enum UpdateType { + BATCHER, + GAS_CONFIG, + GAS_LIMIT, + UNSAFE_BLOCK_SIGNER + } + + /// @notice Struct representing the addresses of L1 system contracts. These should be the + /// proxies and will differ for each OP Stack chain. + struct Addresses { + address l1CrossDomainMessenger; + address l1ERC721Bridge; + address l1StandardBridge; + address l2OutputOracle; + address optimismPortal; + address optimismMintableERC20Factory; + } + + /// @notice Version identifier, used for upgrades. + uint256 public constant VERSION = 0; + + /// @notice Storage slot that the unsafe block signer is stored at. + /// Storing it at this deterministic storage slot allows for decoupling the storage + /// layout from the way that `solc` lays out storage. The `op-node` uses a storage + /// proof to fetch this value. + /// @dev NOTE: this value will be migrated to another storage slot in a future version. + /// User input should not be placed in storage in this contract until this migration + /// happens. It is unlikely that keccak second preimage resistance will be broken, + /// but it is better to be safe than sorry. + bytes32 public constant UNSAFE_BLOCK_SIGNER_SLOT = keccak256("systemconfig.unsafeblocksigner"); + + /// @notice Storage slot that the L1CrossDomainMessenger address is stored at. + bytes32 public constant L1_CROSS_DOMAIN_MESSENGER_SLOT = + bytes32(uint256(keccak256("systemconfig.l1crossdomainmessenger")) - 1); + + /// @notice Storage slot that the L1ERC721Bridge address is stored at. + bytes32 public constant L1_ERC_721_BRIDGE_SLOT = bytes32(uint256(keccak256("systemconfig.l1erc721bridge")) - 1); + + /// @notice Storage slot that the L1StandardBridge address is stored at. + bytes32 public constant L1_STANDARD_BRIDGE_SLOT = bytes32(uint256(keccak256("systemconfig.l1standardbridge")) - 1); + + /// @notice Storage slot that the L2OutputOracle address is stored at. + bytes32 public constant L2_OUTPUT_ORACLE_SLOT = bytes32(uint256(keccak256("systemconfig.l2outputoracle")) - 1); + + /// @notice Storage slot that the OptimismPortal address is stored at. + bytes32 public constant OPTIMISM_PORTAL_SLOT = bytes32(uint256(keccak256("systemconfig.optimismportal")) - 1); + + /// @notice Storage slot that the OptimismMintableERC20Factory address is stored at. + bytes32 public constant OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT = + bytes32(uint256(keccak256("systemconfig.optimismmintableerc20factory")) - 1); + + /// @notice Storage slot that the batch inbox address is stored at. + bytes32 public constant BATCH_INBOX_SLOT = bytes32(uint256(keccak256("systemconfig.batchinbox")) - 1); + + /// @notice Fixed L2 gas overhead. Used as part of the L2 fee calculation. + uint256 public overhead; + + /// @notice Dynamic L2 gas overhead. Used as part of the L2 fee calculation. + uint256 public scalar; + + /// @notice Identifier for the batcher. + /// For version 1 of this configuration, this is represented as an address left-padded + /// with zeros to 32 bytes. + bytes32 public batcherHash; + + /// @notice L2 block gas limit. + uint64 public gasLimit; + + /// @notice The configuration for the deposit fee market. + /// Used by the OptimismPortal to meter the cost of buying L2 gas on L1. + /// Set as internal with a getter so that the struct is returned instead of a tuple. + ResourceMetering.ResourceConfig internal _resourceConfig; + + /// @notice Emitted when configuration is updated. + /// @param version SystemConfig version. + /// @param updateType Type of update. + /// @param data Encoded update data. + event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); + + /// @notice The block at which the op-node can start searching for logs from. + uint256 public startBlock; + + /// @notice Semantic version. + /// @custom:semver 1.10.0 + string public constant version = "1.10.0"; + + /// @notice Constructs the SystemConfig contract. Cannot set + /// the owner to `address(0)` due to the Ownable contract's + /// implementation, so set it to `address(0xdEaD)` + constructor() { + initialize({ + _owner: address(0xdEaD), + _overhead: 0, + _scalar: 0, + _batcherHash: bytes32(0), + _gasLimit: 1, + _unsafeBlockSigner: address(0), + _config: ResourceMetering.ResourceConfig({ + maxResourceLimit: 1, + elasticityMultiplier: 1, + baseFeeMaxChangeDenominator: 2, + minimumBaseFee: 0, + systemTxMaxGas: 0, + maximumBaseFee: 0 + }), + _startBlock: type(uint256).max, + _batchInbox: address(0), + _addresses: SystemConfig.Addresses({ + l1CrossDomainMessenger: address(0), + l1ERC721Bridge: address(0), + l1StandardBridge: address(0), + l2OutputOracle: address(0), + optimismPortal: address(0), + optimismMintableERC20Factory: address(0) + }) + }); + } + + /// @notice Initializer. + /// The resource config must be set before the require check. + /// @param _owner Initial owner of the contract. + /// @param _overhead Initial overhead value. + /// @param _scalar Initial scalar value. + /// @param _batcherHash Initial batcher hash. + /// @param _gasLimit Initial gas limit. + /// @param _unsafeBlockSigner Initial unsafe block signer address. + /// @param _config Initial ResourceConfig. + /// @param _startBlock Starting block for the op-node to search for logs from. + /// Contracts that were deployed before this field existed + /// need to have this field set manually via an override. + /// Newly deployed contracts should set this value to uint256(0). + /// @param _batchInbox Batch inbox address. An identifier for the op-node to find + /// canonical data. + /// @param _addresses Set of L1 contract addresses. These should be the proxies. + function initialize( + address _owner, + uint256 _overhead, + uint256 _scalar, + bytes32 _batcherHash, + uint64 _gasLimit, + address _unsafeBlockSigner, + ResourceMetering.ResourceConfig memory _config, + uint256 _startBlock, + address _batchInbox, + SystemConfig.Addresses memory _addresses + ) public reinitializer(Constants.INITIALIZER) { + __Ownable_init(); + transferOwnership(_owner); + + // These are set in ascending order of their UpdateTypes. + _setBatcherHash(_batcherHash); + _setGasConfig({ _overhead: _overhead, _scalar: _scalar }); + _setGasLimit(_gasLimit); + _setUnsafeBlockSigner(_unsafeBlockSigner); + + Storage.setAddress(BATCH_INBOX_SLOT, _batchInbox); + Storage.setAddress(L1_CROSS_DOMAIN_MESSENGER_SLOT, _addresses.l1CrossDomainMessenger); + Storage.setAddress(L1_ERC_721_BRIDGE_SLOT, _addresses.l1ERC721Bridge); + Storage.setAddress(L1_STANDARD_BRIDGE_SLOT, _addresses.l1StandardBridge); + Storage.setAddress(L2_OUTPUT_ORACLE_SLOT, _addresses.l2OutputOracle); + Storage.setAddress(OPTIMISM_PORTAL_SLOT, _addresses.optimismPortal); + Storage.setAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT, _addresses.optimismMintableERC20Factory); + + _setStartBlock(_startBlock); + + _setResourceConfig(_config); + require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low"); + } + + /// @notice Returns the minimum L2 gas limit that can be safely set for the system to + /// operate. The L2 gas limit must be larger than or equal to the amount of + /// gas that is allocated for deposits per block plus the amount of gas that + /// is allocated for the system transaction. + /// This function is used to determine if changes to parameters are safe. + /// @return uint64 Minimum gas limit. + function minimumGasLimit() public view returns (uint64) { + return uint64(_resourceConfig.maxResourceLimit) + uint64(_resourceConfig.systemTxMaxGas); + } + + /// @notice High level getter for the unsafe block signer address. + /// Unsafe blocks can be propagated across the p2p network if they are signed by the + /// key corresponding to this address. + /// @return addr_ Address of the unsafe block signer. + // solhint-disable-next-line ordering + function unsafeBlockSigner() public view returns (address addr_) { + addr_ = Storage.getAddress(UNSAFE_BLOCK_SIGNER_SLOT); + } + + /// @notice Getter for the L1CrossDomainMessenger address. + function l1CrossDomainMessenger() external view returns (address addr_) { + addr_ = Storage.getAddress(L1_CROSS_DOMAIN_MESSENGER_SLOT); + } + + /// @notice Getter for the L1ERC721Bridge address. + function l1ERC721Bridge() external view returns (address addr_) { + addr_ = Storage.getAddress(L1_ERC_721_BRIDGE_SLOT); + } + + /// @notice Getter for the L1StandardBridge address. + function l1StandardBridge() external view returns (address addr_) { + addr_ = Storage.getAddress(L1_STANDARD_BRIDGE_SLOT); + } + + /// @notice Getter for the L2OutputOracle address. + function l2OutputOracle() external view returns (address addr_) { + addr_ = Storage.getAddress(L2_OUTPUT_ORACLE_SLOT); + } + + /// @notice Getter for the OptimismPortal address. + function optimismPortal() external view returns (address addr_) { + addr_ = Storage.getAddress(OPTIMISM_PORTAL_SLOT); + } + + /// @notice Getter for the OptimismMintableERC20Factory address. + function optimismMintableERC20Factory() external view returns (address addr_) { + addr_ = Storage.getAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT); + } + + /// @notice Getter for the BatchInbox address. + function batchInbox() external view returns (address addr_) { + addr_ = Storage.getAddress(BATCH_INBOX_SLOT); + } + + /// @notice Sets the start block in a backwards compatible way. Proxies + /// that were initialized before the startBlock existed in storage + /// can have their start block set by a user provided override. + /// A start block of 0 indicates that there is no override and the + /// start block will be set by `block.number`. + /// @dev This logic is used to patch legacy deployments with new storage values. + /// Use the override if it is provided as a non zero value and the value + /// has not already been set in storage. Use `block.number` if the value + /// has already been set in storage + /// @param _startBlock The start block override to set in storage. + function _setStartBlock(uint256 _startBlock) internal { + if (_startBlock != 0 && startBlock == 0) { + // There is an override and it is not already set, this is for legacy chains. + startBlock = _startBlock; + } else if (startBlock == 0) { + // There is no override and it is not set in storage. Set it to the block number. + // This is for newly deployed chains. + startBlock = block.number; + } + } + + /// @notice Updates the unsafe block signer address. Can only be called by the owner. + /// @param _unsafeBlockSigner New unsafe block signer address. + function setUnsafeBlockSigner(address _unsafeBlockSigner) external onlyOwner { + _setUnsafeBlockSigner(_unsafeBlockSigner); + } + + /// @notice Updates the unsafe block signer address. + /// @param _unsafeBlockSigner New unsafe block signer address. + function _setUnsafeBlockSigner(address _unsafeBlockSigner) internal { + Storage.setAddress(UNSAFE_BLOCK_SIGNER_SLOT, _unsafeBlockSigner); + + bytes memory data = abi.encode(_unsafeBlockSigner); + emit ConfigUpdate(VERSION, UpdateType.UNSAFE_BLOCK_SIGNER, data); + } + + /// @notice Updates the batcher hash. Can only be called by the owner. + /// @param _batcherHash New batcher hash. + function setBatcherHash(bytes32 _batcherHash) external onlyOwner { + _setBatcherHash(_batcherHash); + } + + /// @notice Internal function for updating the batcher hash. + /// @param _batcherHash New batcher hash. + function _setBatcherHash(bytes32 _batcherHash) internal { + batcherHash = _batcherHash; + + bytes memory data = abi.encode(_batcherHash); + emit ConfigUpdate(VERSION, UpdateType.BATCHER, data); + } + + /// @notice Updates gas config. Can only be called by the owner. + /// @param _overhead New overhead value. + /// @param _scalar New scalar value. + function setGasConfig(uint256 _overhead, uint256 _scalar) external onlyOwner { + _setGasConfig(_overhead, _scalar); + } + + /// @notice Internal function for updating the gas config. + /// @param _overhead New overhead value. + /// @param _scalar New scalar value. + function _setGasConfig(uint256 _overhead, uint256 _scalar) internal { + overhead = _overhead; + scalar = _scalar; + + bytes memory data = abi.encode(_overhead, _scalar); + emit ConfigUpdate(VERSION, UpdateType.GAS_CONFIG, data); + } + + /// @notice Updates the L2 gas limit. Can only be called by the owner. + /// @param _gasLimit New gas limit. + function setGasLimit(uint64 _gasLimit) external onlyOwner { + _setGasLimit(_gasLimit); + } + + /// @notice Internal function for updating the L2 gas limit. + /// @param _gasLimit New gas limit. + function _setGasLimit(uint64 _gasLimit) internal { + require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low"); + gasLimit = _gasLimit; + + bytes memory data = abi.encode(_gasLimit); + emit ConfigUpdate(VERSION, UpdateType.GAS_LIMIT, data); + } + + /// @notice A getter for the resource config. + /// Ensures that the struct is returned instead of a tuple. + /// @return ResourceConfig + function resourceConfig() external view returns (ResourceMetering.ResourceConfig memory) { + return _resourceConfig; + } + + /// @notice An external setter for the resource config. + /// In the future, this method may emit an event that the `op-node` picks up + /// for when the resource config is changed. + /// @param _config The new resource config values. + function setResourceConfig(ResourceMetering.ResourceConfig memory _config) external onlyOwner { + _setResourceConfig(_config); + } + + /// @notice An internal setter for the resource config. + /// Ensures that the config is sane before storing it by checking for invariants. + /// @param _config The new resource config. + function _setResourceConfig(ResourceMetering.ResourceConfig memory _config) internal { + // Min base fee must be less than or equal to max base fee. + require(_config.minimumBaseFee <= _config.maximumBaseFee, "SystemConfig: min base fee must be less than max base"); + // Base fee change denominator must be greater than 1. + require(_config.baseFeeMaxChangeDenominator > 1, "SystemConfig: denominator must be larger than 1"); + // Max resource limit plus system tx gas must be less than or equal to the L2 gas limit. + // The gas limit must be increased before these values can be increased. + require(_config.maxResourceLimit + _config.systemTxMaxGas <= gasLimit, "SystemConfig: gas limit too low"); + // Elasticity multiplier must be greater than 0. + require(_config.elasticityMultiplier > 0, "SystemConfig: elasticity multiplier cannot be 0"); + // No precision loss when computing target resource limit. + require( + ((_config.maxResourceLimit / _config.elasticityMultiplier) * _config.elasticityMultiplier) == + _config.maxResourceLimit, + "SystemConfig: precision loss with target resource limit" + ); + + _resourceConfig = _config; + } +} diff --git a/contracts/src/yield/manager/L2/Blast.sol b/contracts/src/yield/manager/L2/Blast.sol new file mode 100644 index 00000000..d1532fb4 --- /dev/null +++ b/contracts/src/yield/manager/L2/Blast.sol @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import { Semver } from "../universal/Semver.sol"; +import { GasMode, IGas } from "../L2/Gas.sol"; + +enum YieldMode { + AUTOMATIC, + VOID, + CLAIMABLE +} + +interface IYield { + function configure(address contractAddress, uint8 flags) external returns (uint256); + + function claim(address contractAddress, address recipientOfYield, uint256 desiredAmount) external returns (uint256); + + function getClaimableAmount(address contractAddress) external view returns (uint256); + + function getConfiguration(address contractAddress) external view returns (uint8); +} + +interface IBlast { + // configure + function configureContract(address contractAddress, YieldMode _yield, GasMode gasMode, address governor) external; + + function configure(YieldMode _yield, GasMode gasMode, address governor) external; + + // base configuration options + function configureClaimableYield() external; + + function configureClaimableYieldOnBehalf(address contractAddress) external; + + function configureAutomaticYield() external; + + function configureAutomaticYieldOnBehalf(address contractAddress) external; + + function configureVoidYield() external; + + function configureVoidYieldOnBehalf(address contractAddress) external; + + function configureClaimableGas() external; + + function configureClaimableGasOnBehalf(address contractAddress) external; + + function configureVoidGas() external; + + function configureVoidGasOnBehalf(address contractAddress) external; + + function configureGovernor(address _governor) external; + + function configureGovernorOnBehalf(address _newGovernor, address contractAddress) external; + + // claim yield + function claimYield(address contractAddress, address recipientOfYield, uint256 amount) external returns (uint256); + + function claimAllYield(address contractAddress, address recipientOfYield) external returns (uint256); + + // claim gas + function claimAllGas(address contractAddress, address recipientOfGas) external returns (uint256); + + // NOTE: can be off by 1 bip + function claimGasAtMinClaimRate( + address contractAddress, + address recipientOfGas, + uint256 minClaimRateBips + ) external returns (uint256); + + function claimMaxGas(address contractAddress, address recipientOfGas) external returns (uint256); + + function claimGas( + address contractAddress, + address recipientOfGas, + uint256 gasToClaim, + uint256 gasSecondsToConsume + ) external returns (uint256); + + // read functions + function readClaimableYield(address contractAddress) external view returns (uint256); + + function readYieldConfiguration(address contractAddress) external view returns (uint8); + + function readGasParams( + address contractAddress + ) external view returns (uint256 etherSeconds, uint256 etherBalance, uint256 lastUpdated, GasMode); +} + +/// @custom:predeploy 0x4300000000000000000000000000000000000002 +/// @title Blast +contract Blast is IBlast, Initializable, Semver { + address public immutable YIELD_CONTRACT; + address public immutable GAS_CONTRACT; + + mapping(address => address) public governorMap; + + constructor(address _gasContract, address _yieldContract) Semver(1, 0, 0) { + GAS_CONTRACT = _gasContract; + YIELD_CONTRACT = _yieldContract; + _disableInitializers(); + } + + function initialize() public initializer {} + + /** + * @notice Checks if the caller is the governor of the contract + * @param contractAddress The address of the contract + * @return A boolean indicating if the caller is the governor + */ + function isGovernor(address contractAddress) public view returns (bool) { + return msg.sender == governorMap[contractAddress]; + } + + /** + * @notice Checks if the governor is not set for the contract + * @param contractAddress The address of the contract + * @return boolean indicating if the governor is not set + */ + function governorNotSet(address contractAddress) internal view returns (bool) { + return governorMap[contractAddress] == address(0); + } + + /** + * @notice Checks if the caller is authorized + * @param contractAddress The address of the contract + * @return A boolean indicating if the caller is authorized + */ + function isAuthorized(address contractAddress) public view returns (bool) { + return isGovernor(contractAddress) || (governorNotSet(contractAddress) && msg.sender == contractAddress); + } + + /** + * @notice contract configures its yield and gas modes and sets the governor. called by contract + * @param _yieldMode The yield mode to be set + * @param _gasMode The gas mode to be set + * @param governor The address of the governor to be set + */ + function configure(YieldMode _yieldMode, GasMode _gasMode, address governor) external { + // requires that no governor is set for contract + require(isAuthorized(msg.sender), "not authorized to configure contract"); + // set governor + governorMap[msg.sender] = governor; + // set gas mode + IGas(GAS_CONTRACT).setGasMode(msg.sender, _gasMode); + // set yield mode + IYield(YIELD_CONTRACT).configure(msg.sender, uint8(_yieldMode)); + } + + /** + * @notice Configures the yield and gas modes and sets the governor for a specific contract. called by authorized user + * @param contractAddress The address of the contract to be configured + * @param _yieldMode The yield mode to be set + * @param _gasMode The gas mode to be set + * @param _newGovernor The address of the new governor to be set + */ + function configureContract( + address contractAddress, + YieldMode _yieldMode, + GasMode _gasMode, + address _newGovernor + ) external { + // only allow governor, or if no governor is set, the contract itself to configure + require(isAuthorized(contractAddress), "not authorized to configure contract"); + // set governor + governorMap[contractAddress] = _newGovernor; + // set gas mode + IGas(GAS_CONTRACT).setGasMode(contractAddress, _gasMode); + // set yield mode + IYield(YIELD_CONTRACT).configure(contractAddress, uint8(_yieldMode)); + } + + /** + * @notice Configures the yield mode to CLAIMABLE for the contract that calls this function + */ + function configureClaimableYield() external { + require(isAuthorized(msg.sender), "not authorized to configure contract"); + IYield(YIELD_CONTRACT).configure(msg.sender, uint8(YieldMode.CLAIMABLE)); + } + + /** + * @notice Configures the yield mode to CLAIMABLE for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract to be configured + */ + function configureClaimableYieldOnBehalf(address contractAddress) external { + require(isAuthorized(contractAddress), "not authorized to configure contract"); + IYield(YIELD_CONTRACT).configure(contractAddress, uint8(YieldMode.CLAIMABLE)); + } + + /** + * @notice Configures the yield mode to AUTOMATIC for the contract that calls this function + */ + function configureAutomaticYield() external { + require(isAuthorized(msg.sender), "not authorized to configure contract"); + IYield(YIELD_CONTRACT).configure(msg.sender, uint8(YieldMode.AUTOMATIC)); + } + + /** + * @notice Configures the yield mode to AUTOMATIC for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract to be configured + */ + function configureAutomaticYieldOnBehalf(address contractAddress) external { + require(isAuthorized(contractAddress), "not authorized to configure contract"); + IYield(YIELD_CONTRACT).configure(contractAddress, uint8(YieldMode.AUTOMATIC)); + } + + /** + * @notice Configures the yield mode to VOID for the contract that calls this function + */ + function configureVoidYield() external { + require(isAuthorized(msg.sender), "not authorized to configure contract"); + IYield(YIELD_CONTRACT).configure(msg.sender, uint8(YieldMode.VOID)); + } + + /** + * @notice Configures the yield mode to VOID for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract to be configured + */ + function configureVoidYieldOnBehalf(address contractAddress) external { + require(isAuthorized(contractAddress), "not authorized to configure contract"); + IYield(YIELD_CONTRACT).configure(contractAddress, uint8(YieldMode.VOID)); + } + + /** + * @notice Configures the gas mode to CLAIMABLE for the contract that calls this function + */ + function configureClaimableGas() external { + require(isAuthorized(msg.sender), "not authorized to configure contract"); + IGas(GAS_CONTRACT).setGasMode(msg.sender, GasMode.CLAIMABLE); + } + + /** + * @notice Configures the gas mode to CLAIMABLE for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract to be configured + */ + function configureClaimableGasOnBehalf(address contractAddress) external { + require(isAuthorized(contractAddress), "not authorized to configure contract"); + IGas(GAS_CONTRACT).setGasMode(contractAddress, GasMode.CLAIMABLE); + } + + /** + * @notice Configures the gas mode to VOID for the contract that calls this function + */ + function configureVoidGas() external { + require(isAuthorized(msg.sender), "not authorized to configure contract"); + IGas(GAS_CONTRACT).setGasMode(msg.sender, GasMode.VOID); + } + + /** + * @notice Configures the gas mode to void for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract to be configured + */ + function configureVoidGasOnBehalf(address contractAddress) external { + require(isAuthorized(contractAddress), "not authorized to configure contract"); + IGas(GAS_CONTRACT).setGasMode(contractAddress, GasMode.VOID); + } + + /** + * @notice Configures the governor for the contract that calls this function + */ + function configureGovernor(address _governor) external { + require(isAuthorized(msg.sender), "not authorized to configure contract"); + governorMap[msg.sender] = _governor; + } + + /** + * @notice Configures the governor for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract to be configured + */ + function configureGovernorOnBehalf(address _newGovernor, address contractAddress) external { + require(isAuthorized(contractAddress), "not authorized to configure contract"); + governorMap[contractAddress] = _newGovernor; + } + + // claim methods + + /** + * @notice Claims yield for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract for which yield is to be claimed + * @param recipientOfYield The address of the recipient of the yield + * @param amount The amount of yield to be claimed + * @return The amount of yield that was claimed + */ + function claimYield(address contractAddress, address recipientOfYield, uint256 amount) external returns (uint256) { + require(isAuthorized(contractAddress), "Not authorized to claim yield"); + return IYield(YIELD_CONTRACT).claim(contractAddress, recipientOfYield, amount); + } + + /** + * @notice Claims all yield for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract for which all yield is to be claimed + * @param recipientOfYield The address of the recipient of the yield + * @return The amount of yield that was claimed + */ + function claimAllYield(address contractAddress, address recipientOfYield) external returns (uint256) { + require(isAuthorized(contractAddress), "Not authorized to claim yield"); + uint256 amount = IYield(YIELD_CONTRACT).getClaimableAmount(contractAddress); + return IYield(YIELD_CONTRACT).claim(contractAddress, recipientOfYield, amount); + } + + /** + * @notice Claims all gas for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract for which all gas is to be claimed + * @param recipientOfGas The address of the recipient of the gas + * @return The amount of gas that was claimed + */ + function claimAllGas(address contractAddress, address recipientOfGas) external returns (uint256) { + require(isAuthorized(contractAddress), "Not allowed to claim all gas"); + return IGas(GAS_CONTRACT).claimAll(contractAddress, recipientOfGas); + } + + /** + * @notice Claims gas at a minimum claim rate for a specific contract, with error rate '1'. Called by an authorized user + * @param contractAddress The address of the contract for which gas is to be claimed + * @param recipientOfGas The address of the recipient of the gas + * @param minClaimRateBips The minimum claim rate in basis points + * @return The amount of gas that was claimed + */ + function claimGasAtMinClaimRate( + address contractAddress, + address recipientOfGas, + uint256 minClaimRateBips + ) external returns (uint256) { + require(isAuthorized(contractAddress), "Not allowed to claim gas at min claim rate"); + return IGas(GAS_CONTRACT).claimGasAtMinClaimRate(contractAddress, recipientOfGas, minClaimRateBips); + } + + /** + * @notice Claims gas available to be claimed at max claim rate for a specific contract. Called by an authorized user + * @param contractAddress The address of the contract for which maximum gas is to be claimed + * @param recipientOfGas The address of the recipient of the gas + * @return The amount of gas that was claimed + */ + function claimMaxGas(address contractAddress, address recipientOfGas) external returns (uint256) { + require(isAuthorized(contractAddress), "Not allowed to claim max gas"); + return IGas(GAS_CONTRACT).claimMax(contractAddress, recipientOfGas); + } + + /** + * @notice Claims a specific amount of gas for a specific contract. claim rate governed by integral of gas over time + * @param contractAddress The address of the contract for which gas is to be claimed + * @param recipientOfGas The address of the recipient of the gas + * @param gasToClaim The amount of gas to be claimed + * @param gasSecondsToConsume The amount of gas seconds to consume + * @return The amount of gas that was claimed + */ + function claimGas( + address contractAddress, + address recipientOfGas, + uint256 gasToClaim, + uint256 gasSecondsToConsume + ) external returns (uint256) { + require(isAuthorized(contractAddress), "Not allowed to claim gas"); + return IGas(GAS_CONTRACT).claim(contractAddress, recipientOfGas, gasToClaim, gasSecondsToConsume); + } + + /** + * @notice Reads the claimable yield for a specific contract + * @param contractAddress The address of the contract for which the claimable yield is to be read + * @return claimable yield + */ + function readClaimableYield(address contractAddress) public view returns (uint256) { + return IYield(YIELD_CONTRACT).getClaimableAmount(contractAddress); + } + + /** + * @notice Reads the yield configuration for a specific contract + * @param contractAddress The address of the contract for which the yield configuration is to be read + * @return uint8 representing yield enum + */ + function readYieldConfiguration(address contractAddress) public view returns (uint8) { + return IYield(YIELD_CONTRACT).getConfiguration(contractAddress); + } + + /** + * @notice Reads the gas parameters for a specific contract + * @param contractAddress The address of the contract for which the gas parameters are to be read + * @return uint256 representing the accumulated ether seconds + * @return uint256 representing ether balance + * @return uint256 representing last update timestamp + * @return GasMode representing the gas mode (VOID, CLAIMABLE) + */ + function readGasParams(address contractAddress) public view returns (uint256, uint256, uint256, GasMode) { + return IGas(GAS_CONTRACT).readGasParams(contractAddress); + } +} diff --git a/contracts/src/yield/manager/L2/Gas.sol b/contracts/src/yield/manager/L2/Gas.sol new file mode 100644 index 00000000..55ddb741 --- /dev/null +++ b/contracts/src/yield/manager/L2/Gas.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +// import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; +// FIXME: check that the vendorized SafeTransferLib is the same of the original above +import { SafeTransferLib } from "../vendor/solmate/utils/SafeTransferLib.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import { Semver } from "../universal/Semver.sol"; + +enum GasMode { + VOID, + CLAIMABLE +} + +interface IGas { + function readGasParams(address contractAddress) external view returns (uint256, uint256, uint256, GasMode); + + function setGasMode(address contractAddress, GasMode mode) external; + + function claimGasAtMinClaimRate( + address contractAddress, + address recipient, + uint256 minClaimRateBips + ) external returns (uint256); + + function claimAll(address contractAddress, address recipient) external returns (uint256); + + function claimMax(address contractAddress, address recipient) external returns (uint256); + + function claim( + address contractAddress, + address recipient, + uint256 gasToClaim, + uint256 gasSecondsToConsume + ) external returns (uint256); +} + +/// @custom:predeploy 0x4300000000000000000000000000000000000001 +/// @title Gas +contract Gas is IGas, Initializable, Semver { + address public immutable admin; + + // Blast.sol --> controls all dAPP accesses to Gas.sol + address public immutable blastConfigurationContract; + + // BaseFeeVault.sol -> fees from gas claims directed here + address public immutable blastFeeVault; + + // zero claim rate in bps -> percent of gas user is able to claim + // without consuming any gas seconds + uint256 public zeroClaimRate; // bps + + // base claim rate in bps -> percent of gas user is able to claim + // by consuming base gas seconds + uint256 public baseGasSeconds; + uint256 public baseClaimRate; // bps + + // ceil claim rate in bps -> percent of gas user is able to claim + // by consuming ceil gas seconds or more + uint256 public ceilGasSeconds; + uint256 public ceilClaimRate; // bps + + /** + * @notice Constructs the blast gas contract. + * @param _admin The address of the admin. + * @param _blastConfigurationContract The address of the Blast configuration contract. + * @param _blastFeeVault The address of the Blast fee vault. + */ + constructor(address _admin, address _blastConfigurationContract, address _blastFeeVault) Semver(1, 0, 0) { + admin = _admin; + blastConfigurationContract = _blastConfigurationContract; + blastFeeVault = _blastFeeVault; + _disableInitializers(); + } + + /** + * @notice Initializer. + * @param _zeroClaimRate The zero claim rate. + * @param _baseGasSeconds The base gas seconds. + * @param _baseClaimRate The base claim rate. + * @param _ceilGasSeconds The ceiling gas seconds. + * @param _ceilClaimRate The ceiling claim rate. + */ + function initialize( + uint256 _zeroClaimRate, + uint256 _baseGasSeconds, + uint256 _baseClaimRate, + uint256 _ceilGasSeconds, + uint256 _ceilClaimRate + ) public initializer { + require(_zeroClaimRate < _baseClaimRate, "zero claim rate must be < base claim rate"); + require(_baseClaimRate < _ceilClaimRate, "base claim rate must be < ceil claim rate"); + require(_baseGasSeconds < _ceilGasSeconds, "base gas seconds must be < ceil gas seconds"); + require(_baseGasSeconds > 0, "base gas seconds must be > 0"); + require(_ceilClaimRate <= 10000, "ceil claim rate must be less than or equal to 10_000 bips"); + // admin vars + zeroClaimRate = _zeroClaimRate; + baseGasSeconds = _baseGasSeconds; + baseClaimRate = _baseClaimRate; + ceilGasSeconds = _ceilGasSeconds; + ceilClaimRate = _ceilClaimRate; + } + + /** + * @notice Allows only the admin to call a function + */ + modifier onlyAdmin() { + require(msg.sender == admin, "Caller is not the admin"); + _; + } + /** + * @notice Allows only the Blast Configuration Contract to call a function + */ + + modifier onlyBlastConfigurationContract() { + require(msg.sender == blastConfigurationContract, "Caller must be blast configuration contract"); + _; + } + + /** + * @notice Allows the admin to update the parameters + * @param _zeroClaimRate The new zero claim rate + * @param _baseGasSeconds The new base gas seconds + * @param _baseClaimRate The new base claim rate + * @param _ceilGasSeconds The new ceiling gas seconds + * @param _ceilClaimRate The new ceiling claim rate + */ + function updateAdminParameters( + uint256 _zeroClaimRate, + uint256 _baseGasSeconds, + uint256 _baseClaimRate, + uint256 _ceilGasSeconds, + uint256 _ceilClaimRate + ) external onlyAdmin { + require(_zeroClaimRate < _baseClaimRate, "zero claim rate must be < base claim rate"); + require(_baseClaimRate < _ceilClaimRate, "base claim rate must be < ceil claim rate"); + require(_baseGasSeconds < _ceilGasSeconds, "base gas seconds must be < ceil gas seconds"); + require(_baseGasSeconds > 0, "base gas seconds must be > 0"); + require(_ceilClaimRate <= 10000, "ceil claim rate must be less than or equal to 10_000 bips"); + + zeroClaimRate = _zeroClaimRate; + baseGasSeconds = _baseGasSeconds; + baseClaimRate = _baseClaimRate; + ceilGasSeconds = _ceilGasSeconds; + ceilClaimRate = _ceilClaimRate; + } + + /** + * @notice Allows the admin to claim the gas of any address + * @param contractAddress The address of the contract + * @return The amount of ether balance claimed + */ + function adminClaimGas(address contractAddress) external onlyAdmin returns (uint256) { + (, uint256 etherBalance, , ) = readGasParams(contractAddress); + _updateGasParams(contractAddress, 0, 0, GasMode.VOID); + SafeTransferLib.safeTransferETH(blastFeeVault, etherBalance); + return etherBalance; + } + + /** + * @notice Allows an authorized user to set the gas mode for a contract via the BlastConfigurationContract + * @param contractAddress The address of the contract + * @param mode The new gas mode for the contract + */ + function setGasMode(address contractAddress, GasMode mode) external onlyBlastConfigurationContract { + // retrieve gas params + (uint256 etherSeconds, uint256 etherBalance, , ) = readGasParams(contractAddress); + _updateGasParams(contractAddress, etherSeconds, etherBalance, mode); + } + + /** + * @notice Allows a user to claim gas at a minimum claim rate (error = 1 bip) + * @param contractAddress The address of the contract + * @param recipientOfGas The address of the recipient of the gas + * @param minClaimRateBips The minimum claim rate in basis points + * @return The amount of gas claimed + */ + function claimGasAtMinClaimRate( + address contractAddress, + address recipientOfGas, + uint256 minClaimRateBips + ) public returns (uint256) { + require(minClaimRateBips <= ceilClaimRate, "desired claim rate exceeds maximum"); + + (uint256 etherSeconds, uint256 etherBalance, , ) = readGasParams(contractAddress); + if (minClaimRateBips <= zeroClaimRate) { + return claimAll(contractAddress, recipientOfGas); + } + + // set minClaimRate to baseClaimRate in this case + if (minClaimRateBips < baseClaimRate) { + minClaimRateBips = baseClaimRate; + } + + uint256 bipsDiff = minClaimRateBips - baseClaimRate; + uint256 secondsDiff = ceilGasSeconds - baseGasSeconds; + uint256 rateDiff = ceilClaimRate - baseClaimRate; + uint256 minSecondsStaked = baseGasSeconds + Math.ceilDiv(bipsDiff * secondsDiff, rateDiff); + uint256 maxEtherClaimable = etherSeconds / minSecondsStaked; + if (maxEtherClaimable > etherBalance) { + maxEtherClaimable = etherBalance; + } + uint256 secondsToConsume = maxEtherClaimable * minSecondsStaked; + return claim(contractAddress, recipientOfGas, maxEtherClaimable, secondsToConsume); + } + + /** + * @notice Allows a contract to claim all gas + * @param contractAddress The address of the contract + * @param recipientOfGas The address of the recipient of the gas + * @return The amount of gas claimed + */ + function claimAll(address contractAddress, address recipientOfGas) public returns (uint256) { + (uint256 etherSeconds, uint256 etherBalance, , ) = readGasParams(contractAddress); + return claim(contractAddress, recipientOfGas, etherBalance, etherSeconds); + } + + /** + * @notice Allows a contract to claim all gas at the highest possible claim rate + * @param contractAddress The address of the contract + * @param recipientOfGas The address of the recipient of the gas + * @return The amount of gas claimed + */ + function claimMax(address contractAddress, address recipientOfGas) public returns (uint256) { + return claimGasAtMinClaimRate(contractAddress, recipientOfGas, ceilClaimRate); + } + + /** + * @notice Allows a contract to claim a specified amount of gas, at a claim rate set by the number of gas seconds + * @param contractAddress The address of the contract + * @param recipientOfGas The address of the recipient of the gas + * @param gasToClaim The amount of gas to claim + * @param gasSecondsToConsume The amount of gas seconds to consume + * @return The amount of gas claimed (gasToClaim - penalty) + */ + function claim( + address contractAddress, + address recipientOfGas, + uint256 gasToClaim, + uint256 gasSecondsToConsume + ) public onlyBlastConfigurationContract returns (uint256) { + // retrieve gas params + (uint256 etherSeconds, uint256 etherBalance, , GasMode mode) = readGasParams(contractAddress); + + // check validity requirements + require(gasToClaim > 0, "must withdraw non-zero amount"); + require(gasToClaim <= etherBalance, "too much to withdraw"); + require(gasSecondsToConsume <= etherSeconds, "not enough gas seconds"); + + // get claim rate + (uint256 claimRate, uint256 gasSecondsToConsumeNormalized) = getClaimRateBps(gasSecondsToConsume, gasToClaim); + + // calculate tax + uint256 userEther = (gasToClaim * claimRate) / 10_000; + uint256 penalty = gasToClaim - userEther; + + _updateGasParams(contractAddress, etherSeconds - gasSecondsToConsumeNormalized, etherBalance - gasToClaim, mode); + + SafeTransferLib.safeTransferETH(recipientOfGas, userEther); + if (penalty > 0) { + SafeTransferLib.safeTransferETH(blastFeeVault, penalty); + } + + return userEther; + } + + /** + * @notice Calculates the claim rate in basis points based on gasSeconds, gasToClaim + * @param gasSecondsToConsume The amount of gas seconds to consume + * @param gasToClaim The amount of gas to claim + * @return claimRate The calculated claim rate in basis points + * @return gasSecondsToConsume The normalized gas seconds to consume (<= gasSecondsToConsume) + */ + function getClaimRateBps(uint256 gasSecondsToConsume, uint256 gasToClaim) public view returns (uint256, uint256) { + uint256 secondsStaked = gasSecondsToConsume / gasToClaim; + if (secondsStaked < baseGasSeconds) { + return (zeroClaimRate, 0); + } + if (secondsStaked >= ceilGasSeconds) { + uint256 gasToConsumeNormalized = gasToClaim * ceilGasSeconds; + return (ceilClaimRate, gasToConsumeNormalized); + } + + uint256 rateDiff = ceilClaimRate - baseClaimRate; + uint256 secondsDiff = ceilGasSeconds - baseGasSeconds; + uint256 secondsStakedDiff = secondsStaked - baseGasSeconds; + uint256 additionalClaimRate = (rateDiff * secondsStakedDiff) / secondsDiff; + uint256 claimRate = baseClaimRate + additionalClaimRate; + return (claimRate, gasSecondsToConsume); + } + + /** + * @notice Reads the gas parameters for a given user + * @param user The address of the user + * @return etherSeconds The integral of ether over time (ether * seconds vested) + * @return etherBalance The total ether balance for the user + * @return lastUpdated The last updated timestamp for the user's gas parameters + * @return mode The current gas mode for the user + */ + function readGasParams( + address user + ) public view returns (uint256 etherSeconds, uint256 etherBalance, uint256 lastUpdated, GasMode mode) { + bytes32 paramsHash = keccak256(abi.encodePacked(user, "parameters")); + bytes32 packedParams; + // read params + assembly { + packedParams := sload(paramsHash) + } + + // unpack params + // - The first byte (most significant byte) represents the mode + // - The next 12 bytes represent the etherBalance + // - The following 15 bytes represent the etherSeconds + // - The last 4 bytes (least significant bytes) represent the lastUpdated timestamp + mode = GasMode(uint8(packedParams[0])); + etherBalance = uint256((packedParams << (1 * 8)) >> ((32 - 12) * 8)); + etherSeconds = uint256((packedParams << ((1 + 12) * 8)) >> ((32 - 15) * 8)); + lastUpdated = uint256((packedParams << ((1 + 12 + 15) * 8)) >> ((32 - 4) * 8)); + + // update ether seconds + etherSeconds = etherSeconds + etherBalance * (block.timestamp - lastUpdated); + } + + /** + * @notice Updates the gas parameters for a given contract address + * @param contractAddress The address of the contract + * @param etherSeconds The integral of ether over time (ether * seconds vested) + * @param etherBalance The total ether balance for the contract + */ + function _updateGasParams( + address contractAddress, + uint256 etherSeconds, + uint256 etherBalance, + GasMode mode + ) internal { + if (etherBalance >= 1 << (12 * 8) || etherSeconds >= 1 << (15 * 8)) { + revert("Unexpected packing issue due to overflow"); + } + + uint256 updatedTimestamp = block.timestamp; // Known to fit in 4 bytes + + bytes32 paramsHash = keccak256(abi.encodePacked(contractAddress, "parameters")); + bytes32 packedParams; + packedParams = ((bytes32(uint256(mode)) << ((12 + 15 + 4) * 8)) | // Shift mode to the most significant byte + (bytes32(etherBalance) << ((15 + 4) * 8)) | // Shift etherBalance to start after 1 byte of mode + (bytes32(etherSeconds) << (4 * 8)) | // Shift etherSeconds to start after mode and etherBalance + bytes32(updatedTimestamp)); // Keep updatedTimestamp in the least significant bytes + + assembly { + sstore(paramsHash, packedParams) + } + } +} diff --git a/contracts/src/yield/manager/L2/Shares.sol b/contracts/src/yield/manager/L2/Shares.sol new file mode 100644 index 00000000..3abc6cb1 --- /dev/null +++ b/contracts/src/yield/manager/L2/Shares.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import { Semver } from "../universal/Semver.sol"; +import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol"; +import { Predeploys } from "../libraries/Predeploys.sol"; +import { Blast, YieldMode, GasMode } from "../L2/Blast.sol"; + +/// @custom:predeploy 0x4300000000000000000000000000000000000000 +/// @title SharesBase +/// @notice Base contract to track share rebasing and yield reporting. +abstract contract SharesBase is Initializable { + /// @notice Approved yield reporter. + address public immutable REPORTER; + + /// @notice Share price. This value can only increase. + uint256 public price; + + /// @notice Accumulated yield that has not been distributed + /// to the share price. + uint256 public pending; + + /// @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades. + /// A gap size of 48 was chosen here, so that the first slot used in a child contract + /// would be a multiple of 50. + uint256[48] private __gap; + + /// @notice Emitted when a new share price is set after a yield event. + event NewPrice(uint256 price); + + error InvalidReporter(); + error DistributeFailed(uint256 count, uint256 pending); + error PriceIsInitialized(); + + /// @param _reporter Address of the approved yield reporter. + constructor(address _reporter) { + REPORTER = _reporter; + } + + /// @notice Initializer. + /// @param _price Initial share price. + // solhint-disable-next-line func-name-mixedcase + function __SharesBase_init(uint256 _price) internal onlyInitializing { + if (price != 0) { + revert PriceIsInitialized(); + } + price = _price; + } + + /// @notice Get the total number of shares. Needs to be + /// overridden by the child contract. + /// @return Total number of shares. + function count() public view virtual returns (uint256); + + /// @notice Report a yield event and update the share price. + /// @param value Amount of new yield + function addValue(uint256 value) external { + _addValue(value); + } + + function _addValue(uint256 value) internal virtual { + if (AddressAliasHelper.undoL1ToL2Alias(msg.sender) != REPORTER) { + revert InvalidReporter(); + } + + if (value > 0) { + pending += value; + } + + _tryDistributePending(); + } + + /// @notice Attempt to distribute pending yields if there + /// are sufficient pending yields to increase the + /// share price. + /// @return True if there were sufficient pending yields to + /// increase the share price. + function _tryDistributePending() internal returns (bool) { + if (pending < count() || count() == 0) { + return false; + } + + price += pending / count(); + pending = pending % count(); + + emit NewPrice(price); + + return true; + } +} + +/// @custom:predeploy 0x4300000000000000000000000000000000000000 +/// @title Shares +/// @notice Integrated EVM contract to manage native ether share +/// rebasing from yield reports. +contract Shares is SharesBase, Semver { + /// @notice Total number of shares. This value is modified directly + /// by the sequencer EVM. + uint256 private _count; + + /// @notice _reporter Address of approved yield reporter. + constructor(address _reporter) SharesBase(_reporter) Semver(1, 0, 0) { + _disableInitializers(); + } + + /// @notice Initializer. + function initialize(uint256 _price) public initializer { + __SharesBase_init({ _price: _price }); + Blast(Predeploys.BLAST).configureContract(address(this), YieldMode.VOID, GasMode.VOID, address(0xdead)); + /// don't set a governor + } + + /// @inheritdoc SharesBase + function count() public view override returns (uint256) { + return _count; + } + + function _addValue(uint256 value) internal override { + super._addValue(value); + + SharesBase(Predeploys.WETH_REBASING).addValue(value); + } +} diff --git a/contracts/src/yield/manager/libraries/Arithmetic.sol b/contracts/src/yield/manager/libraries/Arithmetic.sol new file mode 100644 index 00000000..869232fb --- /dev/null +++ b/contracts/src/yield/manager/libraries/Arithmetic.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { SignedMath } from "@openzeppelin/contracts/utils/math/SignedMath.sol"; +// FIXME: check if the vendor solady library is the same as the one from solmate +// import {FixedPointMathLib} from "@rari-capital/solmate/src/utils/FixedPointMathLib.sol"; +import { FixedPointMathLib } from "../vendor/solady/FixedPointMathLib.sol"; + +/// @title Arithmetic +/// @notice Even more math than before. + +library Arithmetic { + /// @notice Clamps a value between a minimum and maximum. + /// @param _value The value to clamp. + /// @param _min The minimum value. + /// @param _max The maximum value. + /// @return The clamped value. + function clamp(int256 _value, int256 _min, int256 _max) internal pure returns (int256) { + return SignedMath.min(SignedMath.max(_value, _min), _max); + } + + /// @notice (c)oefficient (d)enominator (exp)onentiation function. + /// Returns the result of: c * (1 - 1/d)^exp. + /// @param _coefficient Coefficient of the function. + /// @param _denominator Fractional denominator. + /// @param _exponent Power function exponent. + /// @return Result of c * (1 - 1/d)^exp. + function cdexp(int256 _coefficient, int256 _denominator, int256 _exponent) internal pure returns (int256) { + return (_coefficient * (FixedPointMathLib.powWad(1e18 - (1e18 / _denominator), _exponent * 1e18))) / 1e18; + } +} diff --git a/contracts/src/yield/manager/libraries/Burn.sol b/contracts/src/yield/manager/libraries/Burn.sol new file mode 100644 index 00000000..681695ef --- /dev/null +++ b/contracts/src/yield/manager/libraries/Burn.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +/// @title Burn +/// @notice Utilities for burning stuff. +library Burn { + /// @notice Burns a given amount of ETH. + /// @param _amount Amount of ETH to burn. + function eth(uint256 _amount) internal { + new Burner{ value: _amount }(); + } + + /// @notice Burns a given amount of gas. + /// @param _amount Amount of gas to burn. + function gas(uint256 _amount) internal view { + uint256 i = 0; + uint256 initialGas = gasleft(); + while (initialGas - gasleft() < _amount) { + ++i; + } + } +} + +/// @title Burner +/// @notice Burner self-destructs on creation and sends all ETH to itself, removing all ETH given to +/// the contract from the circulating supply. Self-destructing is the only way to remove ETH +/// from the circulating supply. +contract Burner { + constructor() payable { + selfdestruct(payable(address(this))); + } +} diff --git a/contracts/src/yield/manager/libraries/Bytes.sol b/contracts/src/yield/manager/libraries/Bytes.sol new file mode 100644 index 00000000..8b075a47 --- /dev/null +++ b/contracts/src/yield/manager/libraries/Bytes.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +/// @title Bytes +/// @notice Bytes is a library for manipulating byte arrays. +library Bytes { + /// @custom:attribution https://github.com/GNSPS/solidity-bytes-utils + /// @notice Slices a byte array with a given starting index and length. Returns a new byte array + /// as opposed to a pointer to the original array. Will throw if trying to slice more + /// bytes than exist in the array. + /// @param _bytes Byte array to slice. + /// @param _start Starting index of the slice. + /// @param _length Length of the slice. + /// @return Slice of the input byte array. + function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) { + unchecked { + require(_length + 31 >= _length, "slice_overflow"); + require(_start + _length >= _start, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + } + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + /// @notice Slices a byte array with a given starting index up to the end of the original byte + /// array. Returns a new array rathern than a pointer to the original. + /// @param _bytes Byte array to slice. + /// @param _start Starting index of the slice. + /// @return Slice of the input byte array. + function slice(bytes memory _bytes, uint256 _start) internal pure returns (bytes memory) { + if (_start >= _bytes.length) { + return bytes(""); + } + return slice(_bytes, _start, _bytes.length - _start); + } + + /// @notice Converts a byte array into a nibble array by splitting each byte into two nibbles. + /// Resulting nibble array will be exactly twice as long as the input byte array. + /// @param _bytes Input byte array to convert. + /// @return Resulting nibble array. + function toNibbles(bytes memory _bytes) internal pure returns (bytes memory) { + bytes memory _nibbles; + assembly { + // Grab a free memory offset for the new array + _nibbles := mload(0x40) + + // Load the length of the passed bytes array from memory + let bytesLength := mload(_bytes) + + // Calculate the length of the new nibble array + // This is the length of the input array times 2 + let nibblesLength := shl(0x01, bytesLength) + + // Update the free memory pointer to allocate memory for the new array. + // To do this, we add the length of the new array + 32 bytes for the array length + // rounded up to the nearest 32 byte boundary to the current free memory pointer. + mstore(0x40, add(_nibbles, and(not(0x1F), add(nibblesLength, 0x3F)))) + + // Store the length of the new array in memory + mstore(_nibbles, nibblesLength) + + // Store the memory offset of the _bytes array's contents on the stack + let bytesStart := add(_bytes, 0x20) + + // Store the memory offset of the nibbles array's contents on the stack + let nibblesStart := add(_nibbles, 0x20) + + // Loop through each byte in the input array + for { + let i := 0x00 + } lt(i, bytesLength) { + i := add(i, 0x01) + } { + // Get the starting offset of the next 2 bytes in the nibbles array + let offset := add(nibblesStart, shl(0x01, i)) + // Load the byte at the current index within the `_bytes` array + let b := byte(0x00, mload(add(bytesStart, i))) + + // Pull out the first nibble and store it in the new array + mstore8(offset, shr(0x04, b)) + // Pull out the second nibble and store it in the new array + mstore8(add(offset, 0x01), and(b, 0x0F)) + } + } + return _nibbles; + } + + /// @notice Compares two byte arrays by comparing their keccak256 hashes. + /// @param _bytes First byte array to compare. + /// @param _other Second byte array to compare. + /// @return True if the two byte arrays are equal, false otherwise. + function equal(bytes memory _bytes, bytes memory _other) internal pure returns (bool) { + return keccak256(_bytes) == keccak256(_other); + } +} diff --git a/contracts/src/yield/manager/libraries/Constants.sol b/contracts/src/yield/manager/libraries/Constants.sol new file mode 100644 index 00000000..63afe072 --- /dev/null +++ b/contracts/src/yield/manager/libraries/Constants.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +import { ResourceMetering } from "../L1/ResourceMetering.sol"; + +/// @title Constants +/// @notice Constants is a library for storing constants. Simple! Don't put everything in here, just +/// the stuff used in multiple contracts. Constants that only apply to a single contract +/// should be defined in that contract instead. +library Constants { + /// @notice Special address to be used as the tx origin for gas estimation calls in the + /// OptimismPortal and CrossDomainMessenger calls. You only need to use this address if + /// the minimum gas limit specified by the user is not actually enough to execute the + /// given message and you're attempting to estimate the actual necessary gas limit. We + /// use address(1) because it's the ecrecover precompile and therefore guaranteed to + /// never have any code on any EVM chain. + address internal constant ESTIMATION_ADDRESS = address(1); + + /// @notice Value used for the L2 sender storage slot in both the OptimismPortal and the + /// CrossDomainMessenger contracts before an actual sender is set. This value is + /// non-zero to reduce the gas cost of message passing transactions. + address internal constant DEFAULT_L2_SENDER = 0x000000000000000000000000000000000000dEaD; + + /// @notice The storage slot that holds the address of a proxy implementation. + /// @dev `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` + bytes32 internal constant PROXY_IMPLEMENTATION_ADDRESS = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /// @notice The storage slot that holds the address of the owner. + /// @dev `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)` + bytes32 internal constant PROXY_OWNER_ADDRESS = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /// @notice Returns the default values for the ResourceConfig. These are the recommended values + /// for a production network. + function DEFAULT_RESOURCE_CONFIG() internal pure returns (ResourceMetering.ResourceConfig memory) { + ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({ + maxResourceLimit: 20_000_000, + elasticityMultiplier: 10, + baseFeeMaxChangeDenominator: 8, + minimumBaseFee: 1 gwei, + systemTxMaxGas: 1_000_000, + maximumBaseFee: type(uint128).max + }); + return config; + } + + /// @notice The `reinitailizer` input for upgradable contracts. This value must be updated + /// each time that the contracts are deployed. + uint8 internal constant INITIALIZER = 1; + + address internal constant YIELD_CONTRACT_ADDRESS = 0x0000000000000000000000000000000000000100; +} diff --git a/contracts/src/yield/manager/libraries/Encoding.sol b/contracts/src/yield/manager/libraries/Encoding.sol new file mode 100644 index 00000000..1107535a --- /dev/null +++ b/contracts/src/yield/manager/libraries/Encoding.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +import { Types } from "./Types.sol"; +import { Hashing } from "./Hashing.sol"; +import { RLPWriter } from "./rlp/RLPWriter.sol"; + +/// @title Encoding +/// @notice Encoding handles Optimism's various different encoding schemes. +library Encoding { + /// @notice RLP encodes the L2 transaction that would be generated when a given deposit is sent + /// to the L2 system. Useful for searching for a deposit in the L2 system. The + /// transaction is prefixed with 0x7e to identify its EIP-2718 type. + /// @param _tx User deposit transaction to encode. + /// @return RLP encoded L2 deposit transaction. + function encodeDepositTransaction(Types.UserDepositTransaction memory _tx) internal pure returns (bytes memory) { + bytes32 source = Hashing.hashDepositSource(_tx.l1BlockHash, _tx.logIndex); + bytes[] memory raw = new bytes[](8); + raw[0] = RLPWriter.writeBytes(abi.encodePacked(source)); + raw[1] = RLPWriter.writeAddress(_tx.from); + raw[2] = _tx.isCreation ? RLPWriter.writeBytes("") : RLPWriter.writeAddress(_tx.to); + raw[3] = RLPWriter.writeUint(_tx.mint); + raw[4] = RLPWriter.writeUint(_tx.value); + raw[5] = RLPWriter.writeUint(uint256(_tx.gasLimit)); + raw[6] = RLPWriter.writeBool(false); + raw[7] = RLPWriter.writeBytes(_tx.data); + return abi.encodePacked(uint8(0x7e), RLPWriter.writeList(raw)); + } + + /// @notice Encodes the cross domain message based on the version that is encoded into the + /// message nonce. + /// @param _nonce Message nonce with version encoded into the first two bytes. + /// @param _sender Address of the sender of the message. + /// @param _target Address of the target of the message. + /// @param _value ETH value to send to the target. + /// @param _gasLimit Gas limit to use for the message. + /// @param _data Data to send with the message. + /// @return Encoded cross domain message. + function encodeCrossDomainMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) internal pure returns (bytes memory) { + (, uint16 version) = decodeVersionedNonce(_nonce); + if (version == 0) { + return encodeCrossDomainMessageV0(_target, _sender, _data, _nonce); + } else if (version == 1) { + return encodeCrossDomainMessageV1(_nonce, _sender, _target, _value, _gasLimit, _data); + } else { + revert("Encoding: unknown cross domain message version"); + } + } + + /// @notice Encodes a cross domain message based on the V0 (legacy) encoding. + /// @param _target Address of the target of the message. + /// @param _sender Address of the sender of the message. + /// @param _data Data to send with the message. + /// @param _nonce Message nonce. + /// @return Encoded cross domain message. + function encodeCrossDomainMessageV0( + address _target, + address _sender, + bytes memory _data, + uint256 _nonce + ) internal pure returns (bytes memory) { + return abi.encodeWithSignature("relayMessage(address,address,bytes,uint256)", _target, _sender, _data, _nonce); + } + + /// @notice Encodes a cross domain message based on the V1 (current) encoding. + /// @param _nonce Message nonce. + /// @param _sender Address of the sender of the message. + /// @param _target Address of the target of the message. + /// @param _value ETH value to send to the target. + /// @param _gasLimit Gas limit to use for the message. + /// @param _data Data to send with the message. + /// @return Encoded cross domain message. + function encodeCrossDomainMessageV1( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) internal pure returns (bytes memory) { + return + abi.encodeWithSignature( + "relayMessage(uint256,address,address,uint256,uint256,bytes)", + _nonce, + _sender, + _target, + _value, + _gasLimit, + _data + ); + } + + /// @notice Adds a version number into the first two bytes of a message nonce. + /// @param _nonce Message nonce to encode into. + /// @param _version Version number to encode into the message nonce. + /// @return Message nonce with version encoded into the first two bytes. + function encodeVersionedNonce(uint240 _nonce, uint16 _version) internal pure returns (uint256) { + uint256 nonce; + assembly { + nonce := or(shl(240, _version), _nonce) + } + return nonce; + } + + /// @notice Pulls the version out of a version-encoded nonce. + /// @param _nonce Message nonce with version encoded into the first two bytes. + /// @return Nonce without encoded version. + /// @return Version of the message. + function decodeVersionedNonce(uint256 _nonce) internal pure returns (uint240, uint16) { + uint240 nonce; + uint16 version; + assembly { + nonce := and(_nonce, 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) + version := shr(240, _nonce) + } + return (nonce, version); + } + + /// @notice Returns an appropriately encoded call to L1Block.setL1BlockValuesEcotone + /// @param baseFeeScalar L1 base fee Scalar + /// @param blobBaseFeeScalar L1 blob base fee Scalar + /// @param sequenceNumber Number of L2 blocks since epoch start. + /// @param timestamp L1 timestamp. + /// @param number L1 blocknumber. + /// @param baseFee L1 base fee. + /// @param blobBaseFee L1 blob base fee. + /// @param hash L1 blockhash. + /// @param batcherHash Versioned hash to authenticate batcher by. + function encodeSetL1BlockValuesEcotone( + uint32 baseFeeScalar, + uint32 blobBaseFeeScalar, + uint64 sequenceNumber, + uint64 timestamp, + uint64 number, + uint256 baseFee, + uint256 blobBaseFee, + bytes32 hash, + bytes32 batcherHash + ) internal pure returns (bytes memory) { + bytes4 functionSignature = bytes4(keccak256("setL1BlockValuesEcotone()")); + return + abi.encodePacked( + functionSignature, + baseFeeScalar, + blobBaseFeeScalar, + sequenceNumber, + timestamp, + number, + baseFee, + blobBaseFee, + hash, + batcherHash + ); + } +} diff --git a/contracts/src/yield/manager/libraries/Hashing.sol b/contracts/src/yield/manager/libraries/Hashing.sol new file mode 100644 index 00000000..8d87394a --- /dev/null +++ b/contracts/src/yield/manager/libraries/Hashing.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +import { Types } from "./Types.sol"; +import { Encoding } from "./Encoding.sol"; + +/// @title Hashing +/// @notice Hashing handles Optimism's various different hashing schemes. +library Hashing { + /// @notice Computes the hash of the RLP encoded L2 transaction that would be generated when a + /// given deposit is sent to the L2 system. Useful for searching for a deposit in the L2 + /// system. + /// @param _tx User deposit transaction to hash. + /// @return Hash of the RLP encoded L2 deposit transaction. + function hashDepositTransaction(Types.UserDepositTransaction memory _tx) internal pure returns (bytes32) { + return keccak256(Encoding.encodeDepositTransaction(_tx)); + } + + /// @notice Computes the deposit transaction's "source hash", a value that guarantees the hash + /// of the L2 transaction that corresponds to a deposit is unique and is + /// deterministically generated from L1 transaction data. + /// @param _l1BlockHash Hash of the L1 block where the deposit was included. + /// @param _logIndex The index of the log that created the deposit transaction. + /// @return Hash of the deposit transaction's "source hash". + function hashDepositSource(bytes32 _l1BlockHash, uint256 _logIndex) internal pure returns (bytes32) { + bytes32 depositId = keccak256(abi.encode(_l1BlockHash, _logIndex)); + return keccak256(abi.encode(bytes32(0), depositId)); + } + + /// @notice Hashes the cross domain message based on the version that is encoded into the + /// message nonce. + /// @param _nonce Message nonce with version encoded into the first two bytes. + /// @param _sender Address of the sender of the message. + /// @param _target Address of the target of the message. + /// @param _value ETH value to send to the target. + /// @param _gasLimit Gas limit to use for the message. + /// @param _data Data to send with the message. + /// @return Hashed cross domain message. + function hashCrossDomainMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) internal pure returns (bytes32) { + (, uint16 version) = Encoding.decodeVersionedNonce(_nonce); + if (version == 0) { + return hashCrossDomainMessageV0(_target, _sender, _data, _nonce); + } else if (version == 1) { + return hashCrossDomainMessageV1(_nonce, _sender, _target, _value, _gasLimit, _data); + } else { + revert("Hashing: unknown cross domain message version"); + } + } + + /// @notice Hashes a cross domain message based on the V0 (legacy) encoding. + /// @param _target Address of the target of the message. + /// @param _sender Address of the sender of the message. + /// @param _data Data to send with the message. + /// @param _nonce Message nonce. + /// @return Hashed cross domain message. + function hashCrossDomainMessageV0( + address _target, + address _sender, + bytes memory _data, + uint256 _nonce + ) internal pure returns (bytes32) { + return keccak256(Encoding.encodeCrossDomainMessageV0(_target, _sender, _data, _nonce)); + } + + /// @notice Hashes a cross domain message based on the V1 (current) encoding. + /// @param _nonce Message nonce. + /// @param _sender Address of the sender of the message. + /// @param _target Address of the target of the message. + /// @param _value ETH value to send to the target. + /// @param _gasLimit Gas limit to use for the message. + /// @param _data Data to send with the message. + /// @return Hashed cross domain message. + function hashCrossDomainMessageV1( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) internal pure returns (bytes32) { + return keccak256(Encoding.encodeCrossDomainMessageV1(_nonce, _sender, _target, _value, _gasLimit, _data)); + } + + /// @notice Derives the withdrawal hash according to the encoding in the L2 Withdrawer contract + /// @param _tx Withdrawal transaction to hash. + /// @return Hashed withdrawal transaction. + function hashWithdrawal(Types.WithdrawalTransaction memory _tx) internal pure returns (bytes32) { + return keccak256(abi.encode(_tx.nonce, _tx.sender, _tx.target, _tx.value, _tx.gasLimit, _tx.data)); + } + + /// @notice Hashes the various elements of an output root proof into an output root hash which + /// can be used to check if the proof is valid. + /// @param _outputRootProof Output root proof which should hash to an output root. + /// @return Hashed output root proof. + function hashOutputRootProof(Types.OutputRootProof memory _outputRootProof) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + _outputRootProof.version, + _outputRootProof.stateRoot, + _outputRootProof.messagePasserStorageRoot, + _outputRootProof.latestBlockhash + ) + ); + } +} diff --git a/contracts/src/yield/manager/libraries/Predeploys.sol b/contracts/src/yield/manager/libraries/Predeploys.sol new file mode 100644 index 00000000..f0f6e3ca --- /dev/null +++ b/contracts/src/yield/manager/libraries/Predeploys.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +/// @title Predeploys +/// @notice Contains constant addresses for contracts that are pre-deployed to the L2 system. +library Predeploys { + /// @notice Address of the L2ToL1MessagePasser predeploy. + address internal constant L2_TO_L1_MESSAGE_PASSER = 0x4200000000000000000000000000000000000016; + + /// @notice Address of the L2CrossDomainMessenger predeploy. + address internal constant L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000007; + + /// @notice Address of the L2StandardBridge predeploy. + address internal constant L2_STANDARD_BRIDGE = 0x4200000000000000000000000000000000000010; + + /// @notice Address of the L2ERC721Bridge predeploy. + address internal constant L2_ERC721_BRIDGE = 0x4200000000000000000000000000000000000014; + + //// @notice Address of the SequencerFeeWallet predeploy. + address internal constant SEQUENCER_FEE_WALLET = 0x4200000000000000000000000000000000000011; + + /// @notice Address of the OptimismMintableERC20Factory predeploy. + address internal constant OPTIMISM_MINTABLE_ERC20_FACTORY = 0x4200000000000000000000000000000000000012; + + /// @notice Address of the OptimismMintableERC721Factory predeploy. + address internal constant OPTIMISM_MINTABLE_ERC721_FACTORY = 0x4200000000000000000000000000000000000017; + + /// @notice Address of the L1Block predeploy. + address internal constant L1_BLOCK_ATTRIBUTES = 0x4200000000000000000000000000000000000015; + + /// @notice Address of the GasPriceOracle predeploy. Includes fee information + /// and helpers for computing the L1 portion of the transaction fee. + address internal constant GAS_PRICE_ORACLE = 0x420000000000000000000000000000000000000F; + + /// @custom:legacy + /// @notice Address of the L1MessageSender predeploy. Deprecated. Use L2CrossDomainMessenger + /// or access tx.origin (or msg.sender) in a L1 to L2 transaction instead. + address internal constant L1_MESSAGE_SENDER = 0x4200000000000000000000000000000000000001; + + /// @custom:legacy + /// @notice Address of the DeployerWhitelist predeploy. No longer active. + address internal constant DEPLOYER_WHITELIST = 0x4200000000000000000000000000000000000002; + + /// @custom:legacy + /// @notice Address of the LegacyERC20ETH predeploy. Deprecated. Balances are migrated to the + /// state trie as of the Bedrock upgrade. Contract has been locked and write functions + /// can no longer be accessed. + address internal constant LEGACY_ERC20_ETH = 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000; + + /// @custom:legacy + /// @notice Address of the L1BlockNumber predeploy. Deprecated. Use the L1Block predeploy + /// instead, which exposes more information about the L1 state. + address internal constant L1_BLOCK_NUMBER = 0x4200000000000000000000000000000000000013; + + /// @custom:legacy + /// @notice Address of the LegacyMessagePasser predeploy. Deprecate. Use the updated + /// L2ToL1MessagePasser contract instead. + address internal constant LEGACY_MESSAGE_PASSER = 0x4200000000000000000000000000000000000000; + + /// @notice Address of the ProxyAdmin predeploy. + address internal constant PROXY_ADMIN = 0x4200000000000000000000000000000000000018; + + /// @notice Address of the BaseFeeVault predeploy. + address internal constant BASE_FEE_VAULT = 0x4200000000000000000000000000000000000019; + + /// @notice Address of the L1FeeVault predeploy. + address internal constant L1_FEE_VAULT = 0x420000000000000000000000000000000000001A; + + /// @notice Address of the GovernanceToken predeploy. + address internal constant GOVERNANCE_TOKEN = 0x4200000000000000000000000000000000000042; + + /// @notice Address of the SchemaRegistry predeploy. + address internal constant SCHEMA_REGISTRY = 0x4200000000000000000000000000000000000020; + + /// @notice Address of the EAS predeploy. + address internal constant EAS = 0x4200000000000000000000000000000000000021; + + /// @notice Address of the Shares predeploy. + address internal constant SHARES = 0x4300000000000000000000000000000000000000; + + /// @notice Address of the Gas predeploy. + address internal constant GAS = 0x4300000000000000000000000000000000000001; + + /// @notice Address of the Blast predeploy. + address internal constant BLAST = 0x4300000000000000000000000000000000000002; + + /// @notice Address of the USDB predeploy. + address internal constant USDB = 0x4300000000000000000000000000000000000003; + + /// @notice Address of the WETH predeploy. + address internal constant WETH_REBASING = 0x4300000000000000000000000000000000000004; + + /// @notice Address of the L2BlastBridge predeploy. + address internal constant L2_BLAST_BRIDGE = 0x4300000000000000000000000000000000000005; +} diff --git a/contracts/src/yield/manager/libraries/SafeCall.sol b/contracts/src/yield/manager/libraries/SafeCall.sol new file mode 100644 index 00000000..633f36ff --- /dev/null +++ b/contracts/src/yield/manager/libraries/SafeCall.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +/// @title SafeCall +/// @notice Perform low level safe calls +library SafeCall { + /// @notice Performs a low level call without copying any returndata. + /// @dev Passes no calldata to the call context. + /// @param _target Address to call + /// @param _gas Amount of gas to pass to the call + /// @param _value Amount of value to pass to the call + function send(address _target, uint256 _gas, uint256 _value) internal returns (bool) { + bool _success; + assembly { + _success := call( + _gas, // gas + _target, // recipient + _value, // ether value + 0, // inloc + 0, // inlen + 0, // outloc + 0 // outlen + ) + } + return _success; + } + + /// @notice Perform a low level call without copying any returndata + /// @param _target Address to call + /// @param _gas Amount of gas to pass to the call + /// @param _value Amount of value to pass to the call + /// @param _calldata Calldata to pass to the call + function call(address _target, uint256 _gas, uint256 _value, bytes memory _calldata) internal returns (bool) { + bool _success; + assembly { + _success := call( + _gas, // gas + _target, // recipient + _value, // ether value + add(_calldata, 32), // inloc + mload(_calldata), // inlen + 0, // outloc + 0 // outlen + ) + } + return _success; + } + + /// @notice Helper function to determine if there is sufficient gas remaining within the context + /// to guarantee that the minimum gas requirement for a call will be met as well as + /// optionally reserving a specified amount of gas for after the call has concluded. + /// @param _minGas The minimum amount of gas that may be passed to the target context. + /// @param _reservedGas Optional amount of gas to reserve for the caller after the execution + /// of the target context. + /// @return `true` if there is enough gas remaining to safely supply `_minGas` to the target + /// context as well as reserve `_reservedGas` for the caller after the execution of + /// the target context. + /// @dev !!!!! FOOTGUN ALERT !!!!! + /// 1.) The 40_000 base buffer is to account for the worst case of the dynamic cost of the + /// `CALL` opcode's `address_access_cost`, `positive_value_cost`, and + /// `value_to_empty_account_cost` factors with an added buffer of 5,700 gas. It is + /// still possible to self-rekt by initiating a withdrawal with a minimum gas limit + /// that does not account for the `memory_expansion_cost` & `code_execution_cost` + /// factors of the dynamic cost of the `CALL` opcode. + /// 2.) This function should *directly* precede the external call if possible. There is an + /// added buffer to account for gas consumed between this check and the call, but it + /// is only 5,700 gas. + /// 3.) Because EIP-150 ensures that a maximum of 63/64ths of the remaining gas in the call + /// frame may be passed to a subcontext, we need to ensure that the gas will not be + /// truncated. + /// 4.) Use wisely. This function is not a silver bullet. + function hasMinGas(uint256 _minGas, uint256 _reservedGas) internal view returns (bool) { + bool _hasMinGas; + assembly { + // Equation: gas × 63 ≥ minGas × 64 + 63(40_000 + reservedGas) + _hasMinGas := iszero(lt(mul(gas(), 63), add(mul(_minGas, 64), mul(add(40000, _reservedGas), 63)))) + } + return _hasMinGas; + } + + /// @notice Perform a low level call without copying any returndata. This function + /// will revert if the call cannot be performed with the specified minimum + /// gas. + /// @param _target Address to call + /// @param _minGas The minimum amount of gas that may be passed to the call + /// @param _value Amount of value to pass to the call + /// @param _calldata Calldata to pass to the call + function callWithMinGas( + address _target, + uint256 _minGas, + uint256 _value, + bytes memory _calldata + ) internal returns (bool) { + bool _success; + bool _hasMinGas = hasMinGas(_minGas, 0); + assembly { + // Assertion: gasleft() >= (_minGas * 64) / 63 + 40_000 + if iszero(_hasMinGas) { + // Store the "Error(string)" selector in scratch space. + mstore(0, 0x08c379a0) + // Store the pointer to the string length in scratch space. + mstore(32, 32) + // Store the string. + // + // SAFETY: + // - We pad the beginning of the string with two zero bytes as well as the + // length (24) to ensure that we override the free memory pointer at offset + // 0x40. This is necessary because the free memory pointer is likely to + // be greater than 1 byte when this function is called, but it is incredibly + // unlikely that it will be greater than 3 bytes. As for the data within + // 0x60, it is ensured that it is 0 due to 0x60 being the zero offset. + // - It's fine to clobber the free memory pointer, we're reverting. + mstore(88, 0x0000185361666543616c6c3a204e6f7420656e6f75676820676173) + + // Revert with 'Error("SafeCall: Not enough gas")' + revert(28, 100) + } + + // The call will be supplied at least ((_minGas * 64) / 63) gas due to the + // above assertion. This ensures that, in all circumstances (except for when the + // `_minGas` does not account for the `memory_expansion_cost` and `code_execution_cost` + // factors of the dynamic cost of the `CALL` opcode), the call will receive at least + // the minimum amount of gas specified. + _success := call( + gas(), // gas + _target, // recipient + _value, // ether value + add(_calldata, 32), // inloc + mload(_calldata), // inlen + 0x00, // outloc + 0x00 // outlen + ) + } + return _success; + } +} diff --git a/contracts/src/yield/manager/libraries/Storage.sol b/contracts/src/yield/manager/libraries/Storage.sol new file mode 100644 index 00000000..745e6594 --- /dev/null +++ b/contracts/src/yield/manager/libraries/Storage.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +/// @title Storage +/// @notice Storage handles reading and writing to arbitary storage locations +library Storage { + /// @notice Returns an address stored in an arbitrary storage slot. + /// These storage slots decouple the storage layout from + /// solc's automation. + /// @param _slot The storage slot to retrieve the address from. + function getAddress(bytes32 _slot) internal view returns (address addr_) { + assembly { + addr_ := sload(_slot) + } + } + + /// @notice Stores an address in an arbitrary storage slot, `_slot`. + /// @param _slot The storage slot to store the address in. + /// @param _address The protocol version to store + /// @dev WARNING! This function must be used cautiously, as it allows for overwriting addresses + /// in arbitrary storage slots. + function setAddress(bytes32 _slot, address _address) internal { + assembly { + sstore(_slot, _address) + } + } + + /// @notice Returns a uint256 stored in an arbitrary storage slot. + /// These storage slots decouple the storage layout from + /// solc's automation. + /// @param _slot The storage slot to retrieve the address from. + function getUint(bytes32 _slot) internal view returns (uint256 value_) { + assembly { + value_ := sload(_slot) + } + } + + /// @notice Stores a value in an arbitrary storage slot, `_slot`. + /// @param _slot The storage slot to store the address in. + /// @param _value The protocol version to store + /// @dev WARNING! This function must be used cautiously, as it allows for overwriting values + /// in arbitrary storage slots. + function setUint(bytes32 _slot, uint256 _value) internal { + assembly { + sstore(_slot, _value) + } + } + + /// @notice Returns a bytes32 stored in an arbitrary storage slot. + /// These storage slots decouple the storage layout from + /// solc's automation. + /// @param _slot The storage slot to retrieve the address from. + function getBytes32(bytes32 _slot) internal view returns (bytes32 value_) { + assembly { + value_ := sload(_slot) + } + } + + /// @notice Stores a bytes32 value in an arbitrary storage slot, `_slot`. + /// @param _slot The storage slot to store the address in. + /// @param _value The protocol version to store + /// @dev WARNING! This function must be used cautiously, as it allows for overwriting values + /// in arbitrary storage slots. + function setBytes32(bytes32 _slot, bytes32 _value) internal { + assembly { + sstore(_slot, _value) + } + } +} diff --git a/contracts/src/yield/manager/libraries/Types.sol b/contracts/src/yield/manager/libraries/Types.sol new file mode 100644 index 00000000..090227f6 --- /dev/null +++ b/contracts/src/yield/manager/libraries/Types.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +/// @title Types +/// @notice Contains various types used throughout the Optimism contract system. +library Types { + /// @notice OutputProposal represents a commitment to the L2 state. The timestamp is the L1 + /// timestamp that the output root is posted. This timestamp is used to verify that the + /// finalization period has passed since the output root was submitted. + /// @custom:field outputRoot Hash of the L2 output. + /// @custom:field timestamp Timestamp of the L1 block that the output root was submitted in. + /// @custom:field l2BlockNumber L2 block number that the output corresponds to. + struct OutputProposal { + bytes32 outputRoot; + uint128 timestamp; + uint128 l2BlockNumber; + } + + /// @notice Struct representing the elements that are hashed together to generate an output root + /// which itself represents a snapshot of the L2 state. + /// @custom:field version Version of the output root. + /// @custom:field stateRoot Root of the state trie at the block of this output. + /// @custom:field messagePasserStorageRoot Root of the message passer storage trie. + /// @custom:field latestBlockhash Hash of the block this output was generated from. + struct OutputRootProof { + bytes32 version; + bytes32 stateRoot; + bytes32 messagePasserStorageRoot; + bytes32 latestBlockhash; + } + + /// @notice Struct representing a deposit transaction (L1 => L2 transaction) created by an end + /// user (as opposed to a system deposit transaction generated by the system). + /// @custom:field from Address of the sender of the transaction. + /// @custom:field to Address of the recipient of the transaction. + /// @custom:field isCreation True if the transaction is a contract creation. + /// @custom:field value Value to send to the recipient. + /// @custom:field mint Amount of ETH to mint. + /// @custom:field gasLimit Gas limit of the transaction. + /// @custom:field data Data of the transaction. + /// @custom:field l1BlockHash Hash of the block the transaction was submitted in. + /// @custom:field logIndex Index of the log in the block the transaction was submitted in. + struct UserDepositTransaction { + address from; + address to; + bool isCreation; + uint256 value; + uint256 mint; + uint64 gasLimit; + bytes data; + bytes32 l1BlockHash; + uint256 logIndex; + } + + /// @notice Struct representing a withdrawal transaction. + /// @custom:field nonce Nonce of the withdrawal transaction + /// @custom:field sender Address of the sender of the transaction. + /// @custom:field target Address of the recipient of the transaction. + /// @custom:field value Value to send to the recipient. + /// @custom:field gasLimit Gas limit of the transaction. + /// @custom:field data Data of the transaction. + struct WithdrawalTransaction { + uint256 nonce; + address sender; + address target; + uint256 value; + uint256 gasLimit; + bytes data; + } +} diff --git a/contracts/src/yield/manager/libraries/rlp/RLPReader.sol b/contracts/src/yield/manager/libraries/rlp/RLPReader.sol new file mode 100644 index 00000000..a1913956 --- /dev/null +++ b/contracts/src/yield/manager/libraries/rlp/RLPReader.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.8; + +/// @custom:attribution https://github.com/hamdiallam/Solidity-RLP +/// @title RLPReader +/// @notice RLPReader is a library for parsing RLP-encoded byte arrays into Solidity types. Adapted +/// from Solidity-RLP (https://github.com/hamdiallam/Solidity-RLP) by Hamdi Allam with +/// various tweaks to improve readability. +library RLPReader { + /// @notice Custom pointer type to avoid confusion between pointers and uint256s. + type MemoryPointer is uint256; + + /// @notice RLP item types. + /// @custom:value DATA_ITEM Represents an RLP data item (NOT a list). + /// @custom:value LIST_ITEM Represents an RLP list item. + enum RLPItemType { + DATA_ITEM, + LIST_ITEM + } + + /// @notice Struct representing an RLP item. + /// @custom:field length Length of the RLP item. + /// @custom:field ptr Pointer to the RLP item in memory. + struct RLPItem { + uint256 length; + MemoryPointer ptr; + } + + /// @notice Max list length that this library will accept. + uint256 internal constant MAX_LIST_LENGTH = 32; + + /// @notice Converts bytes to a reference to memory position and length. + /// @param _in Input bytes to convert. + /// @return out_ Output memory reference. + function toRLPItem(bytes memory _in) internal pure returns (RLPItem memory out_) { + // Empty arrays are not RLP items. + require(_in.length > 0, "RLPReader: length of an RLP item must be greater than zero to be decodable"); + + MemoryPointer ptr; + assembly { + ptr := add(_in, 32) + } + + out_ = RLPItem({ length: _in.length, ptr: ptr }); + } + + /// @notice Reads an RLP list value into a list of RLP items. + /// @param _in RLP list value. + /// @return out_ Decoded RLP list items. + function readList(RLPItem memory _in) internal pure returns (RLPItem[] memory out_) { + (uint256 listOffset, uint256 listLength, RLPItemType itemType) = _decodeLength(_in); + + require(itemType == RLPItemType.LIST_ITEM, "RLPReader: decoded item type for list is not a list item"); + + require(listOffset + listLength == _in.length, "RLPReader: list item has an invalid data remainder"); + + // Solidity in-memory arrays can't be increased in size, but *can* be decreased in size by + // writing to the length. Since we can't know the number of RLP items without looping over + // the entire input, we'd have to loop twice to accurately size this array. It's easier to + // simply set a reasonable maximum list length and decrease the size before we finish. + out_ = new RLPItem[](MAX_LIST_LENGTH); + + uint256 itemCount = 0; + uint256 offset = listOffset; + while (offset < _in.length) { + (uint256 itemOffset, uint256 itemLength, ) = _decodeLength( + RLPItem({ length: _in.length - offset, ptr: MemoryPointer.wrap(MemoryPointer.unwrap(_in.ptr) + offset) }) + ); + + // We don't need to check itemCount < out.length explicitly because Solidity already + // handles this check on our behalf, we'd just be wasting gas. + out_[itemCount] = RLPItem({ + length: itemLength + itemOffset, + ptr: MemoryPointer.wrap(MemoryPointer.unwrap(_in.ptr) + offset) + }); + + itemCount += 1; + offset += itemOffset + itemLength; + } + + // Decrease the array size to match the actual item count. + assembly { + mstore(out_, itemCount) + } + } + + /// @notice Reads an RLP list value into a list of RLP items. + /// @param _in RLP list value. + /// @return out_ Decoded RLP list items. + function readList(bytes memory _in) internal pure returns (RLPItem[] memory out_) { + out_ = readList(toRLPItem(_in)); + } + + /// @notice Reads an RLP bytes value into bytes. + /// @param _in RLP bytes value. + /// @return out_ Decoded bytes. + function readBytes(RLPItem memory _in) internal pure returns (bytes memory out_) { + (uint256 itemOffset, uint256 itemLength, RLPItemType itemType) = _decodeLength(_in); + + require(itemType == RLPItemType.DATA_ITEM, "RLPReader: decoded item type for bytes is not a data item"); + + require(_in.length == itemOffset + itemLength, "RLPReader: bytes value contains an invalid remainder"); + + out_ = _copy(_in.ptr, itemOffset, itemLength); + } + + /// @notice Reads an RLP bytes value into bytes. + /// @param _in RLP bytes value. + /// @return out_ Decoded bytes. + function readBytes(bytes memory _in) internal pure returns (bytes memory out_) { + out_ = readBytes(toRLPItem(_in)); + } + + /// @notice Reads the raw bytes of an RLP item. + /// @param _in RLP item to read. + /// @return out_ Raw RLP bytes. + function readRawBytes(RLPItem memory _in) internal pure returns (bytes memory out_) { + out_ = _copy(_in.ptr, 0, _in.length); + } + + /// @notice Decodes the length of an RLP item. + /// @param _in RLP item to decode. + /// @return offset_ Offset of the encoded data. + /// @return length_ Length of the encoded data. + /// @return type_ RLP item type (LIST_ITEM or DATA_ITEM). + function _decodeLength( + RLPItem memory _in + ) private pure returns (uint256 offset_, uint256 length_, RLPItemType type_) { + // Short-circuit if there's nothing to decode, note that we perform this check when + // the user creates an RLP item via toRLPItem, but it's always possible for them to bypass + // that function and create an RLP item directly. So we need to check this anyway. + require(_in.length > 0, "RLPReader: length of an RLP item must be greater than zero to be decodable"); + + MemoryPointer ptr = _in.ptr; + uint256 prefix; + assembly { + prefix := byte(0, mload(ptr)) + } + + if (prefix <= 0x7f) { + // Single byte. + return (0, 1, RLPItemType.DATA_ITEM); + } else if (prefix <= 0xb7) { + // Short string. + + // slither-disable-next-line variable-scope + uint256 strLen = prefix - 0x80; + + require(_in.length > strLen, "RLPReader: length of content must be greater than string length (short string)"); + + bytes1 firstByteOfContent; + assembly { + firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) + } + + require( + strLen != 1 || firstByteOfContent >= 0x80, + "RLPReader: invalid prefix, single byte < 0x80 are not prefixed (short string)" + ); + + return (1, strLen, RLPItemType.DATA_ITEM); + } else if (prefix <= 0xbf) { + // Long string. + uint256 lenOfStrLen = prefix - 0xb7; + + require( + _in.length > lenOfStrLen, + "RLPReader: length of content must be > than length of string length (long string)" + ); + + bytes1 firstByteOfContent; + assembly { + firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) + } + + require(firstByteOfContent != 0x00, "RLPReader: length of content must not have any leading zeros (long string)"); + + uint256 strLen; + assembly { + strLen := shr(sub(256, mul(8, lenOfStrLen)), mload(add(ptr, 1))) + } + + require(strLen > 55, "RLPReader: length of content must be greater than 55 bytes (long string)"); + + require( + _in.length > lenOfStrLen + strLen, + "RLPReader: length of content must be greater than total length (long string)" + ); + + return (1 + lenOfStrLen, strLen, RLPItemType.DATA_ITEM); + } else if (prefix <= 0xf7) { + // Short list. + // slither-disable-next-line variable-scope + uint256 listLen = prefix - 0xc0; + + require(_in.length > listLen, "RLPReader: length of content must be greater than list length (short list)"); + + return (1, listLen, RLPItemType.LIST_ITEM); + } else { + // Long list. + uint256 lenOfListLen = prefix - 0xf7; + + require( + _in.length > lenOfListLen, + "RLPReader: length of content must be > than length of list length (long list)" + ); + + bytes1 firstByteOfContent; + assembly { + firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) + } + + require(firstByteOfContent != 0x00, "RLPReader: length of content must not have any leading zeros (long list)"); + + uint256 listLen; + assembly { + listLen := shr(sub(256, mul(8, lenOfListLen)), mload(add(ptr, 1))) + } + + require(listLen > 55, "RLPReader: length of content must be greater than 55 bytes (long list)"); + + require( + _in.length > lenOfListLen + listLen, + "RLPReader: length of content must be greater than total length (long list)" + ); + + return (1 + lenOfListLen, listLen, RLPItemType.LIST_ITEM); + } + } + + /// @notice Copies the bytes from a memory location. + /// @param _src Pointer to the location to read from. + /// @param _offset Offset to start reading from. + /// @param _length Number of bytes to read. + /// @return out_ Copied bytes. + function _copy(MemoryPointer _src, uint256 _offset, uint256 _length) private pure returns (bytes memory out_) { + out_ = new bytes(_length); + if (_length == 0) { + return out_; + } + + // Mostly based on Solidity's copy_memory_to_memory: + // solhint-disable max-line-length + // https://github.com/ethereum/solidity/blob/34dd30d71b4da730488be72ff6af7083cf2a91f6/libsolidity/codegen/YulUtilFunctions.cpp#L102-L114 + uint256 src = MemoryPointer.unwrap(_src) + _offset; + assembly { + let dest := add(out_, 32) + let i := 0 + for { + + } lt(i, _length) { + i := add(i, 32) + } { + mstore(add(dest, i), mload(add(src, i))) + } + + if gt(i, _length) { + mstore(add(dest, _length), 0) + } + } + } +} diff --git a/contracts/src/yield/manager/libraries/rlp/RLPWriter.sol b/contracts/src/yield/manager/libraries/rlp/RLPWriter.sol new file mode 100644 index 00000000..ebf8c52d --- /dev/null +++ b/contracts/src/yield/manager/libraries/rlp/RLPWriter.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +/// @custom:attribution https://github.com/bakaoh/solidity-rlp-encode +/// @title RLPWriter +/// @author RLPWriter is a library for encoding Solidity types to RLP bytes. Adapted from Bakaoh's +/// RLPEncode library (https://github.com/bakaoh/solidity-rlp-encode) with minor +/// modifications to improve legibility. +library RLPWriter { + /// @notice RLP encodes a byte string. + /// @param _in The byte string to encode. + /// @return out_ The RLP encoded string in bytes. + function writeBytes(bytes memory _in) internal pure returns (bytes memory out_) { + if (_in.length == 1 && uint8(_in[0]) < 128) { + out_ = _in; + } else { + out_ = abi.encodePacked(_writeLength(_in.length, 128), _in); + } + } + + /// @notice RLP encodes a list of RLP encoded byte byte strings. + /// @param _in The list of RLP encoded byte strings. + /// @return list_ The RLP encoded list of items in bytes. + function writeList(bytes[] memory _in) internal pure returns (bytes memory list_) { + list_ = _flatten(_in); + list_ = abi.encodePacked(_writeLength(list_.length, 192), list_); + } + + /// @notice RLP encodes a string. + /// @param _in The string to encode. + /// @return out_ The RLP encoded string in bytes. + function writeString(string memory _in) internal pure returns (bytes memory out_) { + out_ = writeBytes(bytes(_in)); + } + + /// @notice RLP encodes an address. + /// @param _in The address to encode. + /// @return out_ The RLP encoded address in bytes. + function writeAddress(address _in) internal pure returns (bytes memory out_) { + out_ = writeBytes(abi.encodePacked(_in)); + } + + /// @notice RLP encodes a uint. + /// @param _in The uint256 to encode. + /// @return out_ The RLP encoded uint256 in bytes. + function writeUint(uint256 _in) internal pure returns (bytes memory out_) { + out_ = writeBytes(_toBinary(_in)); + } + + /// @notice RLP encodes a bool. + /// @param _in The bool to encode. + /// @return out_ The RLP encoded bool in bytes. + function writeBool(bool _in) internal pure returns (bytes memory out_) { + out_ = new bytes(1); + out_[0] = (_in ? bytes1(0x01) : bytes1(0x80)); + } + + /// @notice Encode the first byte and then the `len` in binary form if `length` is more than 55. + /// @param _len The length of the string or the payload. + /// @param _offset 128 if item is string, 192 if item is list. + /// @return out_ RLP encoded bytes. + function _writeLength(uint256 _len, uint256 _offset) private pure returns (bytes memory out_) { + if (_len < 56) { + out_ = new bytes(1); + out_[0] = bytes1(uint8(_len) + uint8(_offset)); + } else { + uint256 lenLen; + uint256 i = 1; + while (_len / i != 0) { + lenLen++; + i *= 256; + } + + out_ = new bytes(lenLen + 1); + out_[0] = bytes1(uint8(lenLen) + uint8(_offset) + 55); + for (i = 1; i <= lenLen; i++) { + out_[i] = bytes1(uint8((_len / (256 ** (lenLen - i))) % 256)); + } + } + } + + /// @notice Encode integer in big endian binary form with no leading zeroes. + /// @param _x The integer to encode. + /// @return out_ RLP encoded bytes. + function _toBinary(uint256 _x) private pure returns (bytes memory out_) { + bytes memory b = abi.encodePacked(_x); + + uint256 i = 0; + for (; i < 32; i++) { + if (b[i] != 0) { + break; + } + } + + out_ = new bytes(32 - i); + for (uint256 j = 0; j < out_.length; j++) { + out_[j] = b[i++]; + } + } + + /// @custom:attribution https://github.com/Arachnid/solidity-stringutils + /// @notice Copies a piece of memory to another location. + /// @param _dest Destination location. + /// @param _src Source location. + /// @param _len Length of memory to copy. + function _memcpy(uint256 _dest, uint256 _src, uint256 _len) private pure { + uint256 dest = _dest; + uint256 src = _src; + uint256 len = _len; + + for (; len >= 32; len -= 32) { + assembly { + mstore(dest, mload(src)) + } + dest += 32; + src += 32; + } + + uint256 mask; + unchecked { + mask = 256 ** (32 - len) - 1; + } + assembly { + let srcpart := and(mload(src), not(mask)) + let destpart := and(mload(dest), mask) + mstore(dest, or(destpart, srcpart)) + } + } + + /// @custom:attribution https://github.com/sammayo/solidity-rlp-encoder + /// @notice Flattens a list of byte strings into one byte string. + /// @param _list List of byte strings to flatten. + /// @return out_ The flattened byte string. + function _flatten(bytes[] memory _list) private pure returns (bytes memory out_) { + if (_list.length == 0) { + return new bytes(0); + } + + uint256 len; + uint256 i = 0; + for (; i < _list.length; i++) { + len += _list[i].length; + } + + out_ = new bytes(len); + uint256 flattenedPtr; + assembly { + flattenedPtr := add(out_, 0x20) + } + + for (i = 0; i < _list.length; i++) { + bytes memory item = _list[i]; + + uint256 listPtr; + assembly { + listPtr := add(item, 0x20) + } + + _memcpy(flattenedPtr, listPtr, item.length); + flattenedPtr += _list[i].length; + } + } +} diff --git a/contracts/src/yield/manager/libraries/trie/MerkleTrie.sol b/contracts/src/yield/manager/libraries/trie/MerkleTrie.sol new file mode 100644 index 00000000..d977cce2 --- /dev/null +++ b/contracts/src/yield/manager/libraries/trie/MerkleTrie.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +import { Bytes } from "../Bytes.sol"; +import { RLPReader } from "../rlp/RLPReader.sol"; + +/// @title MerkleTrie +/// @notice MerkleTrie is a small library for verifying standard Ethereum Merkle-Patricia trie +/// inclusion proofs. By default, this library assumes a hexary trie. One can change the +/// trie radix constant to support other trie radixes. +library MerkleTrie { + /// @notice Struct representing a node in the trie. + /// @custom:field encoded The RLP-encoded node. + /// @custom:field decoded The RLP-decoded node. + struct TrieNode { + bytes encoded; + RLPReader.RLPItem[] decoded; + } + + /// @notice Determines the number of elements per branch node. + uint256 internal constant TREE_RADIX = 16; + + /// @notice Branch nodes have TREE_RADIX elements and one value element. + uint256 internal constant BRANCH_NODE_LENGTH = TREE_RADIX + 1; + + /// @notice Leaf nodes and extension nodes have two elements, a `path` and a `value`. + uint256 internal constant LEAF_OR_EXTENSION_NODE_LENGTH = 2; + + /// @notice Prefix for even-nibbled extension node paths. + uint8 internal constant PREFIX_EXTENSION_EVEN = 0; + + /// @notice Prefix for odd-nibbled extension node paths. + uint8 internal constant PREFIX_EXTENSION_ODD = 1; + + /// @notice Prefix for even-nibbled leaf node paths. + uint8 internal constant PREFIX_LEAF_EVEN = 2; + + /// @notice Prefix for odd-nibbled leaf node paths. + uint8 internal constant PREFIX_LEAF_ODD = 3; + + /// @notice Verifies a proof that a given key/value pair is present in the trie. + /// @param _key Key of the node to search for, as a hex string. + /// @param _value Value of the node to search for, as a hex string. + /// @param _proof Merkle trie inclusion proof for the desired node. Unlike traditional Merkle + /// trees, this proof is executed top-down and consists of a list of RLP-encoded + /// nodes that make a path down to the target node. + /// @param _root Known root of the Merkle trie. Used to verify that the included proof is + /// correctly constructed. + /// @return valid_ Whether or not the proof is valid. + function verifyInclusionProof( + bytes memory _key, + bytes memory _value, + bytes[] memory _proof, + bytes32 _root + ) internal pure returns (bool valid_) { + valid_ = Bytes.equal(_value, get(_key, _proof, _root)); + } + + /// @notice Retrieves the value associated with a given key. + /// @param _key Key to search for, as hex bytes. + /// @param _proof Merkle trie inclusion proof for the key. + /// @param _root Known root of the Merkle trie. + /// @return value_ Value of the key if it exists. + function get(bytes memory _key, bytes[] memory _proof, bytes32 _root) internal pure returns (bytes memory value_) { + require(_key.length > 0, "MerkleTrie: empty key"); + + TrieNode[] memory proof = _parseProof(_proof); + bytes memory key = Bytes.toNibbles(_key); + bytes memory currentNodeID = abi.encodePacked(_root); + uint256 currentKeyIndex = 0; + + // Proof is top-down, so we start at the first element (root). + for (uint256 i = 0; i < proof.length; i++) { + TrieNode memory currentNode = proof[i]; + + // Key index should never exceed total key length or we'll be out of bounds. + require(currentKeyIndex <= key.length, "MerkleTrie: key index exceeds total key length"); + + if (currentKeyIndex == 0) { + // First proof element is always the root node. + require( + Bytes.equal(abi.encodePacked(keccak256(currentNode.encoded)), currentNodeID), + "MerkleTrie: invalid root hash" + ); + } else if (currentNode.encoded.length >= 32) { + // Nodes 32 bytes or larger are hashed inside branch nodes. + require( + Bytes.equal(abi.encodePacked(keccak256(currentNode.encoded)), currentNodeID), + "MerkleTrie: invalid large internal hash" + ); + } else { + // Nodes smaller than 32 bytes aren't hashed. + require(Bytes.equal(currentNode.encoded, currentNodeID), "MerkleTrie: invalid internal node hash"); + } + + if (currentNode.decoded.length == BRANCH_NODE_LENGTH) { + if (currentKeyIndex == key.length) { + // Value is the last element of the decoded list (for branch nodes). There's + // some ambiguity in the Merkle trie specification because bytes(0) is a + // valid value to place into the trie, but for branch nodes bytes(0) can exist + // even when the value wasn't explicitly placed there. Geth treats a value of + // bytes(0) as "key does not exist" and so we do the same. + value_ = RLPReader.readBytes(currentNode.decoded[TREE_RADIX]); + require(value_.length > 0, "MerkleTrie: value length must be greater than zero (branch)"); + + // Extra proof elements are not allowed. + require(i == proof.length - 1, "MerkleTrie: value node must be last node in proof (branch)"); + + return value_; + } else { + // We're not at the end of the key yet. + // Figure out what the next node ID should be and continue. + uint8 branchKey = uint8(key[currentKeyIndex]); + RLPReader.RLPItem memory nextNode = currentNode.decoded[branchKey]; + currentNodeID = _getNodeID(nextNode); + currentKeyIndex += 1; + } + } else if (currentNode.decoded.length == LEAF_OR_EXTENSION_NODE_LENGTH) { + bytes memory path = _getNodePath(currentNode); + uint8 prefix = uint8(path[0]); + uint8 offset = 2 - (prefix % 2); + bytes memory pathRemainder = Bytes.slice(path, offset); + bytes memory keyRemainder = Bytes.slice(key, currentKeyIndex); + uint256 sharedNibbleLength = _getSharedNibbleLength(pathRemainder, keyRemainder); + + // Whether this is a leaf node or an extension node, the path remainder MUST be a + // prefix of the key remainder (or be equal to the key remainder) or the proof is + // considered invalid. + require( + pathRemainder.length == sharedNibbleLength, + "MerkleTrie: path remainder must share all nibbles with key" + ); + + if (prefix == PREFIX_LEAF_EVEN || prefix == PREFIX_LEAF_ODD) { + // Prefix of 2 or 3 means this is a leaf node. For the leaf node to be valid, + // the key remainder must be exactly equal to the path remainder. We already + // did the necessary byte comparison, so it's more efficient here to check that + // the key remainder length equals the shared nibble length, which implies + // equality with the path remainder (since we already did the same check with + // the path remainder and the shared nibble length). + require( + keyRemainder.length == sharedNibbleLength, + "MerkleTrie: key remainder must be identical to path remainder" + ); + + // Our Merkle Trie is designed specifically for the purposes of the Ethereum + // state trie. Empty values are not allowed in the state trie, so we can safely + // say that if the value is empty, the key should not exist and the proof is + // invalid. + value_ = RLPReader.readBytes(currentNode.decoded[1]); + require(value_.length > 0, "MerkleTrie: value length must be greater than zero (leaf)"); + + // Extra proof elements are not allowed. + require(i == proof.length - 1, "MerkleTrie: value node must be last node in proof (leaf)"); + + return value_; + } else if (prefix == PREFIX_EXTENSION_EVEN || prefix == PREFIX_EXTENSION_ODD) { + // Prefix of 0 or 1 means this is an extension node. We move onto the next node + // in the proof and increment the key index by the length of the path remainder + // which is equal to the shared nibble length. + currentNodeID = _getNodeID(currentNode.decoded[1]); + currentKeyIndex += sharedNibbleLength; + } else { + revert("MerkleTrie: received a node with an unknown prefix"); + } + } else { + revert("MerkleTrie: received an unparseable node"); + } + } + + revert("MerkleTrie: ran out of proof elements"); + } + + /// @notice Parses an array of proof elements into a new array that contains both the original + /// encoded element and the RLP-decoded element. + /// @param _proof Array of proof elements to parse. + /// @return proof_ Proof parsed into easily accessible structs. + function _parseProof(bytes[] memory _proof) private pure returns (TrieNode[] memory proof_) { + uint256 length = _proof.length; + proof_ = new TrieNode[](length); + for (uint256 i = 0; i < length; ) { + proof_[i] = TrieNode({ encoded: _proof[i], decoded: RLPReader.readList(_proof[i]) }); + unchecked { + ++i; + } + } + } + + /// @notice Picks out the ID for a node. Node ID is referred to as the "hash" within the + /// specification, but nodes < 32 bytes are not actually hashed. + /// @param _node Node to pull an ID for. + /// @return id_ ID for the node, depending on the size of its contents. + function _getNodeID(RLPReader.RLPItem memory _node) private pure returns (bytes memory id_) { + id_ = _node.length < 32 ? RLPReader.readRawBytes(_node) : RLPReader.readBytes(_node); + } + + /// @notice Gets the path for a leaf or extension node. + /// @param _node Node to get a path for. + /// @return nibbles_ Node path, converted to an array of nibbles. + function _getNodePath(TrieNode memory _node) private pure returns (bytes memory nibbles_) { + nibbles_ = Bytes.toNibbles(RLPReader.readBytes(_node.decoded[0])); + } + + /// @notice Utility; determines the number of nibbles shared between two nibble arrays. + /// @param _a First nibble array. + /// @param _b Second nibble array. + /// @return shared_ Number of shared nibbles. + function _getSharedNibbleLength(bytes memory _a, bytes memory _b) private pure returns (uint256 shared_) { + uint256 max = (_a.length < _b.length) ? _a.length : _b.length; + for (; shared_ < max && _a[shared_] == _b[shared_]; ) { + unchecked { + ++shared_; + } + } + } +} diff --git a/contracts/src/yield/manager/libraries/trie/SecureMerkleTrie.sol b/contracts/src/yield/manager/libraries/trie/SecureMerkleTrie.sol new file mode 100644 index 00000000..a1d4ecbe --- /dev/null +++ b/contracts/src/yield/manager/libraries/trie/SecureMerkleTrie.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +import { MerkleTrie } from "./MerkleTrie.sol"; + +/// @title SecureMerkleTrie +/// @notice SecureMerkleTrie is a thin wrapper around the MerkleTrie library that hashes the input +/// keys. Ethereum's state trie hashes input keys before storing them. +library SecureMerkleTrie { + /// @notice Verifies a proof that a given key/value pair is present in the Merkle trie. + /// @param _key Key of the node to search for, as a hex string. + /// @param _value Value of the node to search for, as a hex string. + /// @param _proof Merkle trie inclusion proof for the desired node. Unlike traditional Merkle + /// trees, this proof is executed top-down and consists of a list of RLP-encoded + /// nodes that make a path down to the target node. + /// @param _root Known root of the Merkle trie. Used to verify that the included proof is + /// correctly constructed. + /// @return valid_ Whether or not the proof is valid. + function verifyInclusionProof( + bytes memory _key, + bytes memory _value, + bytes[] memory _proof, + bytes32 _root + ) internal pure returns (bool valid_) { + bytes memory key = _getSecureKey(_key); + valid_ = MerkleTrie.verifyInclusionProof(key, _value, _proof, _root); + } + + /// @notice Retrieves the value associated with a given key. + /// @param _key Key to search for, as hex bytes. + /// @param _proof Merkle trie inclusion proof for the key. + /// @param _root Known root of the Merkle trie. + /// @return value_ Value of the key if it exists. + function get(bytes memory _key, bytes[] memory _proof, bytes32 _root) internal pure returns (bytes memory value_) { + bytes memory key = _getSecureKey(_key); + value_ = MerkleTrie.get(key, _proof, _root); + } + + /// @notice Computes the hashed version of the input key. + /// @param _key Key to hash. + /// @return hash_ Hashed version of the key. + function _getSecureKey(bytes memory _key) private pure returns (bytes memory hash_) { + hash_ = abi.encodePacked(keccak256(_key)); + } +} diff --git a/contracts/src/yield/manager/mainnet-bridge/DelegateCalls.sol b/contracts/src/yield/manager/mainnet-bridge/DelegateCalls.sol new file mode 100644 index 00000000..9cdcb35d --- /dev/null +++ b/contracts/src/yield/manager/mainnet-bridge/DelegateCalls.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +interface IDelegateCalls { + function payInsurancePremium(uint256) external; + + function withdrawFromInsurance(uint256) external; + + function stake(uint256) external; + + function unstake(uint256) external returns (uint256, uint256); + + function preCommitYieldReportDelegateCallHook() external; +} + +abstract contract DelegateCalls { + function _delegatecall_payInsurancePremium(address provider, uint256 arg) internal { + (bool success, ) = provider.delegatecall(abi.encodeCall(IDelegateCalls.payInsurancePremium, (arg))); + require(success, "delegatecall failed"); + } + + function _delegatecall_withdrawFromInsurance(address provider, uint256 arg) internal { + (bool success, ) = provider.delegatecall(abi.encodeCall(IDelegateCalls.withdrawFromInsurance, (arg))); + require(success, "delegatecall failed"); + } + + function _delegatecall_stake(address provider, uint256 arg) internal { + (bool success, ) = provider.delegatecall(abi.encodeCall(IDelegateCalls.stake, (arg))); + require(success, "delegatecall failed"); + } + + function _delegatecall_unstake(address provider, uint256 arg) internal returns (uint256, uint256) { + (bool success, bytes memory res) = provider.delegatecall(abi.encodeCall(IDelegateCalls.unstake, (arg))); + require(success, "delegatecall failed"); + return abi.decode(res, (uint256, uint256)); + } + + function _delegatecall_preCommitYieldReportDelegateCallHook(address provider) internal { + (bool success, ) = provider.delegatecall(abi.encodeCall(IDelegateCalls.preCommitYieldReportDelegateCallHook, ())); + require(success, "delegatecall failed"); + } +} diff --git a/contracts/src/yield/manager/mainnet-bridge/ETHYieldManager.sol b/contracts/src/yield/manager/mainnet-bridge/ETHYieldManager.sol new file mode 100644 index 00000000..a323d097 --- /dev/null +++ b/contracts/src/yield/manager/mainnet-bridge/ETHYieldManager.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { YieldManager } from "../mainnet-bridge/YieldManager.sol"; +import { OptimismPortal } from "../L1/OptimismPortal.sol"; +import { Semver } from "../universal/Semver.sol"; +import { Predeploys } from "../libraries/Predeploys.sol"; + +/// @custom:proxied +/// @title ETHYieldManager +/// @notice Coordinates the accounting, asset management and +/// yield reporting from ETH yield providers. +contract ETHYieldManager is YieldManager, Semver { + error CallerIsNotPortal(); + + constructor() YieldManager(address(0)) Semver(1, 0, 0) { + initialize(OptimismPortal(payable(address(0))), address(0)); + } + + receive() external payable {} + + /// @notice initializer + /// @param _portal Address of the OptimismPortal. + /// @param _owner Address of the YieldManager owner. + function initialize(OptimismPortal _portal, address _owner) public initializer { + __YieldManager_init(_portal, _owner); + } + + /// @inheritdoc YieldManager + function tokenBalance() public view override returns (uint256) { + return address(this).balance; + } + + /// @notice Wrapper for WithdrawalQueue._requestWithdrawal + function requestWithdrawal(uint256 amount) external returns (uint256) { + if (msg.sender != address(portal)) { + revert CallerIsNotPortal(); + } + return _requestWithdrawal(address(portal), amount); + } + + /// @notice Sends the yield report to the Shares contract. + /// @param data Calldata to send in the message. + function _reportYield(bytes memory data) internal override { + portal.depositTransaction(Predeploys.SHARES, 0, REPORT_YIELD_DEFAULT_GAS_LIMIT, false, data); + } +} diff --git a/contracts/src/yield/manager/mainnet-bridge/USDConversions.sol b/contracts/src/yield/manager/mainnet-bridge/USDConversions.sol new file mode 100644 index 00000000..90ebfa71 --- /dev/null +++ b/contracts/src/yield/manager/mainnet-bridge/USDConversions.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +import { YieldManager } from "../mainnet-bridge/YieldManager.sol"; + +interface IUSDT { + function approve(address spender, uint256 amount) external; + + function balanceOf(address) external view returns (uint256); +} + +interface IDssPsm { + function sellGem(address usr, uint256 gemAmt) external; + + function buyGem(address usr, uint256 gemAmt) external; + + function gemJoin() external view returns (address); +} + +interface ICurve3Pool { + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external; +} + +/// @title USDConversions +/// @notice Stateless helper module for converting between USD tokens (DAI/USDC/USDT). +/// +/// DAI and USDC are converted 1-to-1 using Maker's Peg Stability Mechanism. +/// All other tokens conversions are completed through Curve's 3Pool. +library USDConversions { + uint256 constant WAD_DECIMALS = 18; + uint256 constant USD_DECIMALS = 6; + int128 constant DAI_INDEX = 0; + int128 constant USDC_INDEX = 1; + int128 constant USDT_INDEX = 2; + + IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IUSDT constant USDT = IUSDT(0xdAC17F958D2ee523a2206206994597C13D831ec7); + IDssPsm constant PSM = IDssPsm(0x89B78CfA322F6C5dE0aBcEecab66Aee45393cC5A); + ICurve3Pool constant CURVE_3POOL = ICurve3Pool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + /// @notice immutable address of PSM's GemJoin contract + address constant GEM_JOIN = 0x0A59649758aa4d66E25f08Dd01271e891fe52199; + + error InsufficientBalance(); + error MinimumAmountNotMet(); + error IncorrectInputAmountUsed(); + error UnsupportedToken(); + error InvalidExtraData(); + error InvalidTokenIndex(); + + /// @notice Initializer + function _init() internal { + USDC.approve(address(CURVE_3POOL), type(uint256).max); + USDC.approve(GEM_JOIN, type(uint256).max); + USDT.approve(address(CURVE_3POOL), type(uint256).max); + DAI.approve(address(CURVE_3POOL), type(uint256).max); + DAI.approve(GEM_JOIN, type(uint256).max); + DAI.approve(address(PSM), type(uint256).max); + } + + /// @notice Convert between the 3 stablecoin tokens using Curve's 3Pool and Maker's + /// Peg Stability Mechanism. + /// @param inputToken Input token index. + /// @param outputToken Output token index. + /// @param inputAmountWad Input amount in WAD. + /// @param minOutputAmountWad Minimum amount of output token accepted in WAD. + /// @return amountReceived Amount of output token received in the token's + /// decimal representation. + function _convert( + int128 inputToken, + int128 outputToken, + uint256 inputAmountWad, + uint256 minOutputAmountWad + ) internal returns (uint256 amountReceived) { + require(inputToken >= 0 && inputToken < 3 && outputToken >= 0 && outputToken < 3); + require(inputToken != outputToken); + if (inputAmountWad > 0) { + uint256 inputAmount = _convertDecimals(inputAmountWad, inputToken); + uint256 minOutputAmount = _convertDecimals(minOutputAmountWad, outputToken); + if (_tokenBalance(inputToken) < inputAmount) { + revert InsufficientBalance(); + } + uint256 beforeBalance = _tokenBalance(outputToken); + if (inputToken == USDC_INDEX && outputToken == DAI_INDEX) { + PSM.sellGem(address(this), inputAmount); + } else if (inputToken == DAI_INDEX && outputToken == USDC_INDEX) { + uint256 beforeInputBalance = _tokenBalance(inputToken); + PSM.buyGem(address(this), _wadToUSD(minOutputAmountWad)); // buyGem expects the input amount in USDC + uint256 amountSent = beforeInputBalance - _tokenBalance(inputToken); + if (amountSent != inputAmountWad) { + revert IncorrectInputAmountUsed(); + } + } else { + CURVE_3POOL.exchange(inputToken, outputToken, inputAmount, minOutputAmount); + } + amountReceived = _tokenBalance(outputToken) - beforeBalance; + if (amountReceived < minOutputAmount) { + revert MinimumAmountNotMet(); + } + } + } + + /// @notice Convert between supported token pairs, reverting if not supported. + /// @param inputTokenAddress Address of the input token. + /// @param outputTokenAddress Address of the output token. + /// @param inputAmountWad Amount of input token to convert in WAD. + /// @param _extraData Extra data containing the minimum amount of output token to receive in WAD. + /// @return amountReceived Amount of output token received in WAD. + function _convertTo( + address inputTokenAddress, + address outputTokenAddress, + uint256 inputAmountWad, + bytes memory _extraData + ) internal returns (uint256 amountReceived) { + if (inputTokenAddress == outputTokenAddress) { + return inputAmountWad; + } + + if (outputTokenAddress == address(DAI)) { + return _convertToDAI(inputTokenAddress, inputAmountWad, _extraData); + } else { + revert UnsupportedToken(); + } + } + + /// @notice Convert USDC, USDT, and DAI to DAI. If the input token is DAI, + /// the input amount is returned without conversion. + /// @param inputTokenAddress Address of the input token. + /// @param inputAmountWad Amount of input token to convert in WAD. + /// @param _extraData Extra data containing the minimum amount of USDB to be minted in WAD. + /// Only needed for USDC and USDT. The expected format is: (uint256 minOutputAmountWad). + /// @return amountReceived Amount of DAI received. + function _convertToDAI( + address inputTokenAddress, + uint256 inputAmountWad, + bytes memory _extraData + ) internal returns (uint256 amountReceived) { + if (inputTokenAddress == address(DAI)) { + return inputAmountWad; + } + + if (_extraData.length != 32) { + revert InvalidExtraData(); + } + + uint256 minOutputAmountWad = abi.decode(_extraData, (uint256)); + + if (inputTokenAddress == address(USDC)) { + return USDConversions._convert(USDC_INDEX, DAI_INDEX, inputAmountWad, minOutputAmountWad); + } else if (inputTokenAddress == address(USDT)) { + return USDConversions._convert(USDT_INDEX, DAI_INDEX, inputAmountWad, minOutputAmountWad); + } else { + revert UnsupportedToken(); + } + } + + /// @notice Get the token address from the Curve token index. + /// @param index Curve token index. + /// @return Address of the token. + function _token(int128 index) private pure returns (address) { + if (index == USDC_INDEX) { + return address(USDC); + } else if (index == USDT_INDEX) { + return address(USDT); + } else if (index == DAI_INDEX) { + return address(DAI); + } else { + revert InvalidTokenIndex(); + } + } + + /// @notice Get the contract's token balance from the Curve token index. + /// @param index Curve token index. + /// @return Token balance. + function _tokenBalance(int128 index) internal view returns (uint256) { + if (_token(index) == YieldManager(address(this)).TOKEN()) { + return YieldManager(address(this)).availableBalance(); + } else { + return IERC20(_token(index)).balanceOf(address(this)); + } + } + + /// @notice Convert WAD representation to the token's native decimal representation. + /// USDT and USDC are both 6 decimals and are converted. + /// @param wad Amount in WAD. + /// @param index Curve 3Pool index of the token. + /// @return result Amount in native decimals representation. + function _convertDecimals(uint256 wad, int128 index) internal pure returns (uint256 result) { + if (index == USDT_INDEX || index == USDC_INDEX) { + result = _wadToUSD(wad); + } else { + result = wad; + } + } + + /// @notice Convert value in WAD (18 decimals) to USD (6 decimals). + /// @param wad Amount to convert in WAD. + /// @return Amount in USD. + function _wadToUSD(uint256 wad) internal pure returns (uint256) { + return _convertDecimals(wad, WAD_DECIMALS, USD_DECIMALS); + } + + /// @notice Convert value in USD (6 decimals) to WAD (18 decimals). + /// @param usd Amount to convert in USD. + /// @return Amount in WAD. + function _usdToWad(uint256 usd) internal pure returns (uint256) { + return _convertDecimals(usd, USD_DECIMALS, WAD_DECIMALS); + } + + /// @notice Convert value to desired output decimals representation. + /// @param input Input amount. + /// @param inputDecimals Number of decimals in the input. + /// @param outputDecimals Desired number of decimals in the output. + /// @return `input` in `outputDecimals`. + function _convertDecimals( + uint256 input, + uint256 inputDecimals, + uint256 outputDecimals + ) internal pure returns (uint256) { + if (inputDecimals > outputDecimals) { + return input / (10 ** (inputDecimals - outputDecimals)); + } else { + return input * (10 ** (outputDecimals - inputDecimals)); + } + } +} diff --git a/contracts/src/yield/manager/mainnet-bridge/YieldManager.sol b/contracts/src/yield/manager/mainnet-bridge/YieldManager.sol new file mode 100644 index 00000000..45f0536a --- /dev/null +++ b/contracts/src/yield/manager/mainnet-bridge/YieldManager.sol @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { SignedMath } from "@openzeppelin/contracts/utils/math/SignedMath.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { WithdrawalQueue } from "../mainnet-bridge/withdrawal-queue/WithdrawalQueue.sol"; +import { YieldProvider } from "../mainnet-bridge/yield-providers/YieldProvider.sol"; +import { Types } from "../libraries/Types.sol"; +import { SafeCall } from "../libraries/SafeCall.sol"; +import { CrossDomainMessenger } from "../universal/CrossDomainMessenger.sol"; +import { SharesBase } from "../L2/Shares.sol"; +import { DelegateCalls } from "../mainnet-bridge/DelegateCalls.sol"; +import { USDConversions } from "../mainnet-bridge/USDConversions.sol"; +import { Semver } from "../universal/Semver.sol"; +import { OptimismPortal } from "../L1/OptimismPortal.sol"; +import { Predeploys } from "../libraries/Predeploys.sol"; + +interface IInsurance { + function coverLoss(address token, uint256 amount) external; +} + +/// @title YieldManager +/// @notice Base contract to centralize accounting, asset management and +/// yield reporting from yield providers of a common base asset. +abstract contract YieldManager is Ownable2StepUpgradeable, WithdrawalQueue, DelegateCalls { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice Maximum gas limit for the yield report call on L2. + uint32 internal constant REPORT_YIELD_DEFAULT_GAS_LIMIT = 200_000; + + /// @notice Maximum insurance fee the owner is allowed to set. + uint256 public constant MAX_INSURANCE_FEE_BIPS = 10_000; // 100% + + /// @notice Number of basis points representing 100 percent. + uint256 internal constant BASIS_POINTS = 10_000; + + /// @notice Set of provider addresses. + EnumerableSet.AddressSet private _providers; + + /// @notice Address of the admin handling regular tasks such as + /// `stake`, `unstake`, `claim`, `commitYieldReport`, and + /// `finalize`. + address public admin; + + /// @notice Address of the insurance module. + address public insurance; + + /// @notice Address of the L1BlastBridge. + address public blastBridge; + + /// @notice Sum of negative yields to track the slippage between L2-L1 share price. + /// If negative yields accumulate, L1 withdrawals are discounted to cover the + /// loss. + uint256 public accumulatedNegativeYields; + + /// @notice Current insurance fee in bips. + uint256 public insuranceFeeBips; + + /// @notice Amount of additional funds to withdraw from insurance. + /// This buffer addresses the scenario where the transfer of the exact amount of accumulated + /// negative yields from insurance does not fully pay off the outstanding amount. In Lido's + /// system, the transfer logic is based on shares, which may lead to discrepancies in the + /// withdrawal of insurance funds. By including this buffer, the system ensures that when + /// insurance funds are withdrawn, the total amount withdrawn is the exact required amount + /// plus an additional buffer. This approach guarantees the complete payoff of any negative + /// yields, accommodating for any potential rounding discrepancies inherent in the share-based + /// transfer logic. + uint256 public insuranceWithdrawalBuffer; + + /// @notice Address of the OptimismPortal. + OptimismPortal public portal; + + /// @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades. + /// A gap size of 41 was chosen here, so that the first slot used in a child contract + /// would be a multiple of 50. + uint256[41] private __gap; + + struct ProviderInfo { + bytes32 id; + address providerAddress; + uint256 stakedBalance; + uint256 pendingBalance; + uint256 stakedPrincipal; + uint256 totalValue; + int256 yield; + } + + /// @notice Emitted when the yield report is committed on L1 and + /// the yield is communicated to L2. + /// @param yield Amount of yield generated at this checkpoint. + /// @param insurancePremiumPaid Amount paid in insurance. + /// @param insuranceWithdrawn Amount withdrawn from insurance. + event YieldReport(int256 yield, uint256 insurancePremiumPaid, uint256 insuranceWithdrawn); + + error CallerIsNotAdmin(); + error FailedToInitializeProvider(); + error ProviderAddressDoesNotMatchIndex(); + error InsufficientInsuranceBalance(); + error NegativeYieldFromInsuredProvider(); + error TotalValueIsZero(); + error CallerIsNotBlastBridge(); + error ProviderNotFound(); + error YieldProviderIsNotMeantForThisManager(); + error NegativeYieldIncrease(); + + modifier onlyAdmin() { + if (msg.sender != admin) { + revert CallerIsNotAdmin(); + } + _; + } + + /// @notice Modifier only allowing the L1BlastBridge to call a function. + modifier onlyBlastBridge() { + if (msg.sender != blastBridge) { + revert CallerIsNotBlastBridge(); + } + _; + } + + /// @param _token Address of withdrawal token. + constructor(address _token) WithdrawalQueue(_token) {} + + /// @notice initializer + /// @param _portal Address of the OptimismPortal. + /// @param _owner Address of the YieldManager owner. + function __YieldManager_init(OptimismPortal _portal, address _owner) internal onlyInitializing { + __Ownable2Step_init(); + __WithdrawalQueue_init(); + _transferOwnership(_owner); + + portal = _portal; + } + + /* ========== OWNER FUNCTIONS ========== */ + + /// @notice Set new admin account to handle regular tasks including + /// (stake, unstake, claim). + /// @param _admin Address of new admin + function setAdmin(address _admin) external onlyOwner { + require(_admin != address(0)); + admin = _admin; + } + + /// @notice Set the yield insurance parameters. + /// @param _insurance Address of the insurance module. + /// @param _insuranceFeeBips Insurance fee to take from positive yields. + /// @param _withdrawalBuffer Amount of additional funds to withdraw from insurance. + function setInsurance(address _insurance, uint256 _insuranceFeeBips, uint256 _withdrawalBuffer) external onlyOwner { + require(_insurance != address(0)); + require(_insuranceFeeBips <= MAX_INSURANCE_FEE_BIPS); + insurance = _insurance; + insuranceFeeBips = _insuranceFeeBips; + insuranceWithdrawalBuffer = _withdrawalBuffer; + } + + /// @notice Set the address of the L1BlastBridge. + /// @param _blastBridge Address of the L1BlastBridge. + function setBlastBridge(address _blastBridge) external onlyOwner { + require(_blastBridge != address(0)); + blastBridge = _blastBridge; + } + + /// @notice Add a yield provider contract. + /// @param provider Address of the yield provider. + function addProvider(address provider) external onlyOwner { + if (address(YieldProvider(provider).YIELD_MANAGER()) != address(this)) { + revert YieldProviderIsNotMeantForThisManager(); + } + _providers.add(provider); + (bool success, ) = provider.delegatecall(abi.encodeWithSignature("initialize()")); + if (!success) { + revert FailedToInitializeProvider(); + } + } + + /// @notice Remove a yield provider contract. + /// @param provider Address of the yield provider. + function removeProvider(address provider) external onlyOwner { + _providers.remove(provider); + } + + /* ========== ADMIN FUNCTIONS ========== */ + + /// @notice Stake funds for a particular yield provider and record the + /// staked deposit. The stake call is made via 'delegatecall' + /// so the yield provider implementation is executed with the + /// yield manager's funds. + /// @param idx Index of the provider. + /// @param providerAddress Address of the provider at index 'idx'. + /// @param amount Amount to stake (wad). + function stake(uint256 idx, address providerAddress, uint256 amount) external onlyAdmin { + if (_providers.at(idx) != providerAddress) { + revert ProviderAddressDoesNotMatchIndex(); + } + _delegatecall_stake(providerAddress, amount); + YieldProvider(providerAddress).recordStakedDeposit(amount); + } + + /// @notice Unstake funds for a particular yield provider and record the + /// staked withdraw. The stake call is made via 'delegatecall' + /// so the yield provider implementation is executed with the + /// yield manager's funds. + /// @param idx Index of the provider. + /// @param providerAddress Address of the provider at index 'idx'. + /// @param amount Amount to stake (wad). + function unstake(uint256 idx, address providerAddress, uint256 amount) external onlyAdmin { + if (_providers.at(idx) != providerAddress) { + revert ProviderAddressDoesNotMatchIndex(); + } + (uint256 pending, uint256 claimed) = _delegatecall_unstake(providerAddress, amount); + YieldProvider(providerAddress).recordUnstaked(pending, claimed, amount); + } + + /// @notice Commit yield report. + /// @param enableInsurance Whether insurance should be taken from positive yields + /// and paid out for negative yields. If false, negative yields will + /// accumulate and withdrawals will be discounted. If true (and insurance + /// is supported by the provider), it will guarantee that committed yield + /// is always non-negative, or else revert. It also guarantees that + /// accumulated negative yields never increase. + function commitYieldReport(bool enableInsurance) public onlyAdmin { + uint256 providersLength = _providers.length(); + uint256 negativeYieldBefore = accumulatedNegativeYields; + uint256 totalInsurancePremiumPaid; + uint256 totalInsuranceWithdrawal; + int256 totalYield; + + // For each provider, commit yield after paying to/from the insurance as necessary + for (uint256 i; i < providersLength; i++) { + // run the pre-commit yield report hook + _delegatecall_preCommitYieldReportDelegateCallHook(_providers.at(i)); + + // read the current yield from the provider + int256 yield = YieldProvider(_providers.at(i)).yield(); + uint256 insurancePayment; + + // take care of insurance payments and withdrawals + if (enableInsurance && YieldProvider(_providers.at(i)).supportsInsurancePayment() && insurance != address(0)) { + if (yield > 0) { + // pay the insurance premium + insurancePayment = (uint256(yield) * insuranceFeeBips) / BASIS_POINTS; + _delegatecall_payInsurancePremium(_providers.at(i), insurancePayment); + totalInsurancePremiumPaid += insurancePayment; + } else if (yield < 0) { + // withdraw from the insurance to cover the loss + uint256 insuranceWithdrawal = SignedMath.abs(yield) + insuranceWithdrawalBuffer; + uint256 insuranceBalance = YieldProvider(_providers.at(i)).insuranceBalance(); + if (insuranceBalance < insuranceWithdrawal) { + revert InsufficientInsuranceBalance(); + } + _delegatecall_withdrawFromInsurance(_providers.at(i), insuranceWithdrawal); + totalInsuranceWithdrawal += insuranceWithdrawal; + } + } + + // Commit the yield for the provider + int256 committedYield = YieldProvider(_providers.at(i)).commitYield(); + + // Sanity check + if (enableInsurance && YieldProvider(_providers.at(i)).supportsInsurancePayment() && insurance != address(0)) { + if (committedYield < 0) { + revert NegativeYieldFromInsuredProvider(); + } + } + + // update totalYield + totalYield += committedYield; + } + + // reflect the accumulated negative yield in totalYield + if (accumulatedNegativeYields > 0) { + totalYield -= SafeCast.toInt256(accumulatedNegativeYields); + } + + emit YieldReport(totalYield, totalInsurancePremiumPaid, totalInsuranceWithdrawal); + + if (totalYield < 0) { + accumulatedNegativeYields = uint256(-1 * totalYield); + } else { + accumulatedNegativeYields = 0; + if (totalYield > 0) { + _reportYield(abi.encodeWithSelector(SharesBase.addValue.selector, totalYield)); + } + } + + if (enableInsurance && accumulatedNegativeYields > negativeYieldBefore) { + revert NegativeYieldIncrease(); + } + } + + /// @notice Helper function to atomically withdraw from insurance and commit yield report. + /// This function can be used to maintain share price = 1e27 when yield from + /// the registered providers is not sufficient to cover negative yield from + /// LidoYieldProvider._claim(). + function commitYieldReportAfterInsuranceWithdrawal(address token, uint256 amount) external onlyAdmin { + require(insurance != address(0)); + IInsurance(insurance).coverLoss(token, amount); + commitYieldReport(true); + } + + /// @notice Report realized negative yield. This is meant to be called inside a YieldProvider + /// method that is executed via 'delegatecall' by the YieldManager. + function recordNegativeYield(uint256 amount) external { + require(msg.sender == address(this), "Caller is not this contract"); + accumulatedNegativeYields += amount; + } + + /// @notice Finalize withdrawal requests up to 'requestId'. + /// @param requestId Last request id to finalize in this batch. + function finalize(uint256 requestId) external onlyAdmin returns (uint256 checkpointId) { + uint256 nominalAmount; + uint256 realAmount; + (nominalAmount, realAmount, checkpointId) = _finalize(requestId, availableBalance(), sharePrice()); + // nominalAmount - realAmount is the share of the accumulated negative yield + // that should be paid by the current withdrawal + if (nominalAmount > realAmount) { + accumulatedNegativeYields = _subClamped(accumulatedNegativeYields, nominalAmount - realAmount); + } + } + + /* ========== VIRTUAL FUNCTIONS ========== */ + + /// @notice Get the amount of the withdrawal token that is held by the yield manager. + function tokenBalance() public view virtual returns (uint256); + + /// @notice Send the yield report to the L2 contract that is responsible for + /// updating the L2 share price. + /// @param data Calldata to send in the message. + function _reportYield(bytes memory data) internal virtual; + + /* ========== VIEW FUNCTIONS ========== */ + + /// @notice Available balance. + function availableBalance() public view returns (uint256) { + return tokenBalance() - getLockedBalance(); + } + + /// @notice Get the total value of all yield providers denominated in the withdrawal token. + function totalProviderValue() public view returns (uint256 sum) { + uint256 providersLength = _providers.length(); + for (uint256 i; i < providersLength; i++) { + sum += YieldProvider(_providers.at(i)).totalValue(); + } + } + + /// @notice Get the total value of all yield providers plus the available balance value. + function totalValue() public view returns (uint256) { + return availableBalance() + totalProviderValue(); + } + + /// @notice Get the share price of the withdrawal token with 1e27 precision. + /// The share price is capped at 1e27 and can only go down if there + /// are accumulated negative yields. + function sharePrice() public view returns (uint256) { + uint256 value = totalValue(); + if (value == 0) { + revert TotalValueIsZero(); + } + return (value * E27_PRECISION_BASE) / (value + accumulatedNegativeYields); + } + + /// @notice Get an accounting report on the current state of a yield provider. + /// Due to how EnumerableSet works, 'idx' is not guaranteed to be stable + /// across add/remove operations so admin should verify the idx before + /// calling state-changing functions (e.g. stake, unstake). + /// @param idx Index of the provider. + /// @return info Accounting report on the yield provider. + function getProviderInfoAt(uint256 idx) external view returns (ProviderInfo memory info) { + YieldProvider provider = YieldProvider(_providers.at(idx)); + + info.id = provider.id(); + info.providerAddress = address(provider); + info.stakedBalance = provider.stakedBalance(); + info.pendingBalance = provider.pendingBalance(); + info.stakedPrincipal = provider.stakedPrincipal(); + info.totalValue = provider.totalValue(); + info.yield = provider.yield(); + } + + /// @notice Record an increase to the staked funds represented + /// by the provider. + /// @param providerAddress Address of yield provider. + /// @param amount Amount of additional staked funds. + function recordStakedDeposit(address providerAddress, uint256 amount) external onlyBlastBridge { + if (!_providers.contains(providerAddress)) { + revert ProviderNotFound(); + } + YieldProvider(providerAddress).recordStakedDeposit(amount); + } + + /// @notice Returns max(0, x - y) without reverting on underflow. + function _subClamped(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = x > y ? x - y : 0; + } + } +} diff --git a/contracts/src/yield/manager/mainnet-bridge/withdrawal-queue/WithdrawalQueue.sol b/contracts/src/yield/manager/mainnet-bridge/withdrawal-queue/WithdrawalQueue.sol new file mode 100644 index 00000000..3759cc42 --- /dev/null +++ b/contracts/src/yield/manager/mainnet-bridge/withdrawal-queue/WithdrawalQueue.sol @@ -0,0 +1,431 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.15; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { SafeCall } from "../../libraries/SafeCall.sol"; + +/// @title WithdrawalQueue +/// @notice Queue for storing and managing withdrawal requests. +/// This contract is based on Lido's WithdrawalQueue and has been +/// modified to support Blast specific logic such as withdrawal discounts. +contract WithdrawalQueue is Initializable { + using EnumerableSet for EnumerableSet.UintSet; + using SafeERC20 for IERC20; + + /// @notice The L1 gas limit set when sending eth to the YieldManager. + uint256 internal constant SEND_DEFAULT_GAS_LIMIT = 100_000; + + /// @notice precision base for share rate + uint256 internal constant E27_PRECISION_BASE = 1e27; + + /// @notice return value for the `find...` methods in case of no result + uint256 internal constant NOT_FOUND = 0; + + address public immutable TOKEN; + + WithdrawalRequest[] private _requests; + mapping(address => EnumerableSet.UintSet) private _requestsByOwner; + Checkpoint[] private _checkpoints; + uint256 private lastRequestId; + uint256 private lastFinalizedRequestId; + uint256 private lastCheckpointId; + uint256 private lockedBalance; + + /// @notice Reserve extra slots (to a total of 50) in the storage layout for future upgrades. + /// A gap size of 42 was chosen here, so that the first slot used in a child contract + /// would be a multiple of 50. + uint256[42] private __gap; + + /// @notice structure representing a request for withdrawal + struct WithdrawalRequest { + /// @notice sum of the all tokens submitted for withdrawals including this request (nominal amount) + uint128 cumulativeAmount; + /// @notice address that can claim the request and receives the funds + address recipient; + /// @notice block.timestamp when the request was created + uint40 timestamp; + /// @notice flag if the request was claimed + bool claimed; + } + + /// @notice output format struct for `_getWithdrawalStatus()` method + struct WithdrawalRequestStatus { + /// @notice nominal token amount that was locked on withdrawal queue for this request + uint256 amount; + /// @notice address that can claim or transfer this request + address recipient; + /// @notice timestamp of when the request was created, in seconds + uint256 timestamp; + /// @notice true, if request is finalized + bool isFinalized; + /// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed) + bool isClaimed; + } + + /// @notice structure to store discounts for requests that are affected by negative rebase + /// All requests covered by the checkpoint are affected by the same discount rate `sharePrice`. + struct Checkpoint { + uint256 fromRequestId; + uint256 sharePrice; + } + + /// @dev amount represents the nominal amount of tokens that were withdrawn (burned) on L2. + event WithdrawalRequested( + uint256 indexed requestId, + address indexed requestor, + address indexed recipient, + uint256 amount + ); + + /// @dev amountOfETHLocked represents the real amount of ETH that was locked in the queue and will be + /// transferred to the recipient on claim. + event WithdrawalsFinalized( + uint256 indexed from, + uint256 indexed to, + uint256 indexed checkpointId, + uint256 amountOfETHLocked, + uint256 timestamp, + uint256 sharePrice + ); + + /// @dev amount represents the real amount of ETH that was transferred to the recipient. + event WithdrawalClaimed(uint256 indexed requestId, address indexed recipient, uint256 amountOfETH); + + error InvalidRequestId(uint256 _requestId); + error InvalidRequestIdRange(uint256 startId, uint256 endId); + error InvalidSharePrice(); + error RequestNotFoundOrNotFinalized(uint256 _requestId); + error RequestAlreadyClaimed(uint256 _requestId); + error InvalidHint(uint256 _hint); + error RequestIdsNotSorted(); + error CallerIsNotRecipient(); + error WithdrawalTransferFailed(); + error InsufficientBalance(); + + constructor(address _token) { + TOKEN = _token; + } + + /// @notice initialize the contract with the dummy request and checkpoint + /// as the zero elements of the corresponding arrays so that + /// the first element of the array has index 1 + function __WithdrawalQueue_init() internal onlyInitializing { + _requests.push(WithdrawalRequest(0, address(0), uint40(block.timestamp), true)); + _checkpoints.push(Checkpoint(0, 0)); + } + + function getWithdrawalStatus( + uint256[] calldata _requestIds + ) external view returns (WithdrawalRequestStatus[] memory statuses) { + statuses = new WithdrawalRequestStatus[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + statuses[i] = _getStatus(_requestIds[i]); + } + } + + function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestIds) { + return _requestsByOwner[_owner].values(); + } + + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hintIds + ) external view returns (uint256[] memory claimableEthValues) { + claimableEthValues = new uint256[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + claimableEthValues[i] = _getClaimableEther(_requestIds[i], _hintIds[i]); + } + } + + function _getClaimableEther(uint256 _requestId, uint256 _hintId) internal view returns (uint256) { + if (_requestId == 0 || _requestId > lastRequestId) revert InvalidRequestId(_requestId); + + if (_requestId > lastFinalizedRequestId) return 0; + + WithdrawalRequest storage request = _requests[_requestId]; + if (request.claimed) return 0; + + return _calculateClaimableEther(_requestId, _hintId); + } + + /// @notice id of the last request + /// NB! requests are indexed from 1, so it returns 0 if there is no requests in the queue + function getLastRequestId() external view returns (uint256) { + return lastRequestId; + } + + /// @notice id of the last finalized request + /// NB! requests are indexed from 1, so it returns 0 if there is no finalized requests in the queue + function getLastFinalizedRequestId() external view returns (uint256) { + return lastFinalizedRequestId; + } + + /// @notice amount of ETH on this contract balance that is locked for withdrawal and available to claim + /// NB! this is the real amount of ETH (i.e. sum of (nominal amount of ETH burned on L2 * sharePrice)) + function getLockedBalance() public view returns (uint256) { + return lockedBalance; + } + + /// @notice return the last checkpoint id in the queue + function getLastCheckpointId() external view returns (uint256) { + return lastCheckpointId; + } + + /// @notice return the number of unfinalized requests in the queue + function unfinalizedRequestNumber() public view returns (uint256) { + return lastRequestId - lastFinalizedRequestId; + } + + /// @notice Returns the amount of ETH in the queue yet to be finalized + /// NB! this is the nominal amount of ETH burned on L2 + function unfinalizedAmount() internal view returns (uint256) { + return _requests[lastRequestId].cumulativeAmount - _requests[lastFinalizedRequestId].cumulativeAmount; + } + + /// @dev Finalize requests in the queue + /// @notice sharePrice has 1e27 precision + /// Emits WithdrawalsFinalized event. + function _finalize( + uint256 _lastRequestIdToBeFinalized, + uint256 availableBalance, + uint256 sharePrice + ) internal returns (uint256 nominalAmountToFinalize, uint256 realAmountToFinalize, uint256 checkpointId) { + // share price cannot be larger than 1e27 + if (sharePrice > E27_PRECISION_BASE) { + revert InvalidSharePrice(); + } + + if (_lastRequestIdToBeFinalized != 0) { + if (_lastRequestIdToBeFinalized > lastRequestId) revert InvalidRequestId(_lastRequestIdToBeFinalized); + uint256 _lastFinalizedRequestId = lastFinalizedRequestId; + if (_lastRequestIdToBeFinalized <= _lastFinalizedRequestId) { + revert InvalidRequestId(_lastRequestIdToBeFinalized); + } + + WithdrawalRequest memory lastFinalizedRequest = _requests[_lastFinalizedRequestId]; + WithdrawalRequest memory requestToFinalize = _requests[_lastRequestIdToBeFinalized]; + + nominalAmountToFinalize = requestToFinalize.cumulativeAmount - lastFinalizedRequest.cumulativeAmount; + realAmountToFinalize = (nominalAmountToFinalize * sharePrice) / E27_PRECISION_BASE; + if (realAmountToFinalize > availableBalance) { + revert InsufficientBalance(); + } + + uint256 firstRequestIdToFinalize = _lastFinalizedRequestId + 1; + + lockedBalance += realAmountToFinalize; + lastFinalizedRequestId = _lastRequestIdToBeFinalized; + + checkpointId = _createCheckpoint(firstRequestIdToFinalize, sharePrice); + + emit WithdrawalsFinalized( + firstRequestIdToFinalize, + _lastRequestIdToBeFinalized, + checkpointId, + realAmountToFinalize, + block.timestamp, + sharePrice + ); + } + } + + /// @notice Finds the list of hints for the given `_requestIds` searching among the checkpoints with indices + /// in the range `[_firstIndex, _lastIndex]`. + /// NB! Array of request ids should be sorted + /// NB! `_firstIndex` should be greater than 0, because checkpoint list is 1-based array + /// Usage: findCheckpointHints(_requestIds, 1, getLastCheckpointIndex()) + /// @param _requestIds ids of the requests sorted in the ascending order to get hints for + /// @param _firstIndex left boundary of the search range. Should be greater than 0 + /// @param _lastIndex right boundary of the search range. Should be less than or equal to getLastCheckpointIndex() + /// @return hintIds array of hints used to find required checkpoint for the request + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + hintIds = new uint256[](_requestIds.length); + uint256 prevRequestId = 0; + for (uint256 i = 0; i < _requestIds.length; ++i) { + if (_requestIds[i] < prevRequestId) { + revert RequestIdsNotSorted(); + } + hintIds[i] = findCheckpointHint(_requestIds[i], _firstIndex, _lastIndex); + _firstIndex = hintIds[i]; + prevRequestId = _requestIds[i]; + } + } + + /// @dev View function to find a checkpoint hint to use in `claimWithdrawal()` and `getClaimableEther()` + /// Search will be performed in the range of `[_firstIndex, _lastIndex]` + /// + /// @param _requestId request id to search the checkpoint for + /// @param _start index of the left boundary of the search range, should be greater than 0 + /// @param _end index of the right boundary of the search range, should be less than or equal + /// to queue.lastCheckpointId + /// + /// @return hint for later use in other methods or 0 if hint not found in the range + function findCheckpointHint(uint256 _requestId, uint256 _start, uint256 _end) public view returns (uint256) { + if (_requestId == 0 || _requestId > lastRequestId) { + revert InvalidRequestId(_requestId); + } + + uint256 lastCheckpointIndex = lastCheckpointId; + if (_start == 0 || _end > lastCheckpointIndex) { + revert InvalidRequestIdRange(_start, _end); + } + + if (lastCheckpointIndex == 0 || _requestId > lastFinalizedRequestId || _start > _end) { + return NOT_FOUND; + } + + // Right boundary + if (_requestId >= _checkpoints[_end].fromRequestId) { + // it's the last checkpoint, so it's valid + if (_end == lastCheckpointIndex) { + return _end; + } + // it fits right before the next checkpoint + if (_requestId < _checkpoints[_end + 1].fromRequestId) { + return _end; + } + + return NOT_FOUND; + } + // Left boundary + if (_requestId < _checkpoints[_start].fromRequestId) { + return NOT_FOUND; + } + + // Binary search + uint256 min = _start; + uint256 max = _end - 1; + + while (max > min) { + uint256 mid = (max + min + 1) / 2; + if (_checkpoints[mid].fromRequestId <= _requestId) { + min = mid; + } else { + max = mid - 1; + } + } + return min; + } + + /// @dev Returns the status of the withdrawal request with `_requestId` id + function _getStatus(uint256 _requestId) internal view returns (WithdrawalRequestStatus memory status) { + if (_requestId == 0 || _requestId > lastRequestId) revert InvalidRequestId(_requestId); + + WithdrawalRequest memory request = _requests[_requestId]; + WithdrawalRequest memory previousRequest = _requests[_requestId - 1]; + + status = WithdrawalRequestStatus( + request.cumulativeAmount - previousRequest.cumulativeAmount, + request.recipient, + request.timestamp, + _requestId <= lastFinalizedRequestId, + request.claimed + ); + } + + /// @dev creates a new `WithdrawalRequest` in the queue + /// Emits WithdrawalRequested event + function _requestWithdrawal(address recipient, uint256 amount) internal returns (uint256 requestId) { + uint256 _lastRequestId = lastRequestId; + WithdrawalRequest memory lastRequest = _requests[_lastRequestId]; + + uint128 cumulativeAmount = lastRequest.cumulativeAmount + SafeCast.toUint128(amount); + + requestId = _lastRequestId + 1; + + lastRequestId = requestId; + + WithdrawalRequest memory newRequest = WithdrawalRequest( + cumulativeAmount, + recipient, + uint40(block.timestamp), + false + ); + _requests.push(newRequest); + _requestsByOwner[recipient].add(requestId); + + emit WithdrawalRequested(requestId, msg.sender, recipient, amount); + } + + /// @dev assumes firstRequestIdToFinalize > _lastFinalizedRequestId && sharePrice <= 1e27 + function _createCheckpoint(uint256 firstRequestIdToFinalize, uint256 sharePrice) internal returns (uint256) { + _checkpoints.push(Checkpoint(firstRequestIdToFinalize, sharePrice)); + lastCheckpointId += 1; + return lastCheckpointId; + } + + /// @dev can only be called by request.recipient (YieldManager) + function claimWithdrawal(uint256 _requestId, uint256 _hintId) external returns (bool success) { + if (_requestId == 0) revert InvalidRequestId(_requestId); + if (_requestId > lastFinalizedRequestId) revert RequestNotFoundOrNotFinalized(_requestId); + + WithdrawalRequest storage request = _requests[_requestId]; + + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + request.claimed = true; + + address recipient = request.recipient; + if (msg.sender != recipient) { + revert CallerIsNotRecipient(); + } + + uint256 realAmount = _calculateClaimableEther(_requestId, _hintId); + lockedBalance -= realAmount; + + if (TOKEN == address(0)) { + (success) = SafeCall.send(recipient, SEND_DEFAULT_GAS_LIMIT, realAmount); + } else { + IERC20(TOKEN).safeTransfer(recipient, realAmount); + success = true; + } + + if (!success) { + revert WithdrawalTransferFailed(); + } + + emit WithdrawalClaimed(_requestId, recipient, realAmount); + } + + /// @dev Calculate the amount of ETH that can be claimed for the withdrawal request with `_requestId`. + /// NB! This function returns the real amount of ETH that can be claimed by the recipient, not the nominal amount + /// that was burned on L2. The real amount is calculated as nominal amount * share price, which can be found + /// in the checkpoint with `_hintId`. + function _calculateClaimableEther(uint256 _requestId, uint256 _hintId) internal view returns (uint256) { + if (_hintId == 0) { + revert InvalidHint(_hintId); + } + + uint256 lastCheckpointIndex = lastCheckpointId; + if (_hintId > lastCheckpointIndex) { + revert InvalidHint(_hintId); + } + + Checkpoint memory checkpoint = _checkpoints[_hintId]; + if (_requestId < checkpoint.fromRequestId) { + revert InvalidHint(_hintId); + } + if (_hintId < lastCheckpointIndex) { + Checkpoint memory nextCheckpoint = _checkpoints[_hintId + 1]; + if (_requestId >= nextCheckpoint.fromRequestId) { + revert InvalidHint(_hintId); + } + } + + WithdrawalRequest storage prevRequest = _requests[_requestId - 1]; + WithdrawalRequest storage request = _requests[_requestId]; + + uint256 nominalAmount = request.cumulativeAmount - prevRequest.cumulativeAmount; + return (nominalAmount * checkpoint.sharePrice) / E27_PRECISION_BASE; + } +} diff --git a/contracts/src/yield/manager/mainnet-bridge/yield-providers/YieldProvider.sol b/contracts/src/yield/manager/mainnet-bridge/yield-providers/YieldProvider.sol new file mode 100644 index 00000000..f74696e1 --- /dev/null +++ b/contracts/src/yield/manager/mainnet-bridge/yield-providers/YieldProvider.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { YieldManager } from "../../mainnet-bridge/YieldManager.sol"; +import { Semver } from "../../universal/Semver.sol"; + +/// @title YieldProvider +/// @notice Base contract for interacting and accounting for a +/// specific yield source. +abstract contract YieldProvider is Semver { + YieldManager public immutable YIELD_MANAGER; + + uint256 public stakedPrincipal; + uint256 public pendingBalance; + + event YieldCommit(bytes32 indexed provider, int256 yield); + event Staked(bytes32 indexed provider, uint256 amount); + event Unstaked(bytes32 indexed provider, uint256 amount); + event Pending(bytes32 indexed provider, uint256 amount); + event Claimed(bytes32 indexed provider, uint256 claimedAmount, uint256 expectedAmount); + event InsurancePremiumPaid(bytes32 indexed provider, uint256 amount); + event InsuranceWithdrawn(bytes32 indexed provider, uint256 amount); + + error InsufficientStakableFunds(); + error CallerIsNotYieldManager(); + error ContextIsNotYieldManager(); + error NotSupported(); + + modifier onlyYieldManager() { + if (msg.sender != address(YIELD_MANAGER)) { + revert CallerIsNotYieldManager(); + } + _; + } + + modifier onlyDelegateCall() { + if (address(this) != address(YIELD_MANAGER)) { + revert ContextIsNotYieldManager(); + } + _; + } + + /// @param _yieldManager Address of the yield manager for the underlying + /// yield asset of this provider. + constructor(YieldManager _yieldManager) Semver(1, 0, 0) { + require(address(_yieldManager) != address(this)); + YIELD_MANAGER = _yieldManager; + } + + /// @notice initialize + function initialize() external virtual onlyDelegateCall {} + + function name() public pure virtual returns (string memory); + + function id() public view returns (bytes32) { + return keccak256(abi.encodePacked(name(), version())); + } + + /// @notice Whether staking is enabled for the given asset. + function isStakingEnabled(address token) external view virtual returns (bool); + + /// @notice Current balance of the provider's staked funds. + function stakedBalance() public view virtual returns (uint256); + + /// @notice Total value in the provider's yield method/protocol. + function totalValue() public view returns (uint256) { + return stakedBalance() + pendingBalance; + } + + /// @notice Current amount of yield gained since the previous commit. + function yield() public view virtual returns (int256); + + /// @notice Whether the provider supports yield insurance. + function supportsInsurancePayment() public view virtual returns (bool) { + return false; + } + + /// @notice Gets insurance balance available for the provider's assets. + function insuranceBalance() public view virtual returns (uint256) { + revert("not supported"); + } + + /// @notice Commit the current amount of yield and checkpoint the accounting + /// variables. + /// @return Amount of yield at this checkpoint. + function commitYield() external onlyYieldManager returns (int256) { + _beforeCommitYield(); + + int256 _yield = yield(); + stakedPrincipal = stakedBalance(); + + _afterCommitYield(); + + emit YieldCommit(id(), _yield); + return _yield; + } + + /// @notice Stake YieldManager funds using the provider's yield method/protocol. + /// Must be called via `delegatecall` from the YieldManager. + function stake(uint256) external virtual; + + /// @notice Unstake YieldManager funds from the provider's yield method/protocol. + /// Must be called via `delegatecall` from the YieldManager. + /// @return pending Amount of funds pending in an unstaking delay + /// @return claimed Amount of funds that have been claimed. + /// The yield provider is expected to return + /// (pending = 0, claimed = non-zero) if the funds are immediately + /// available for withdrawal, and (pending = non-zero, claimed = 0) + /// if the funds are in an unstaking delay. + function unstake(uint256) external virtual returns (uint256 pending, uint256 claimed); + + /// @notice Pay insurance premium during a yield report. Must be called via + /// `delegatecall` from the YieldManager. + function payInsurancePremium(uint256) external virtual onlyDelegateCall { + revert NotSupported(); + } + + /// @notice Withdraw insurance funds to cover yield losses during a yield report. + /// Must be called via `delegatecall` from the YieldManager. + function withdrawFromInsurance(uint256) external virtual onlyDelegateCall { + revert NotSupported(); + } + + /// @notice Record a deposit to the stake balance of the provider to track the + /// principal balance. + /// @param amount Amount of new staked balance to record. + function recordStakedDeposit(uint256 amount) external virtual onlyYieldManager { + stakedPrincipal += amount; + emit Staked(id(), amount); + } + + /// @notice Record a withdraw to the stake balance of the provider to track the + /// principal balance. This method should be called by the Yield Manager + /// after delegate-calling the provider's `unstake` method, which should + /// return the arguments to this method. + function recordUnstaked(uint256 pending, uint256 claimed, uint256 expected) external virtual onlyYieldManager { + _recordStakedWithdraw(expected); + + if (pending > 0) { + require(claimed == 0 && pending == expected, "invalid yield provider implementation"); + _recordPending(pending); + } + + if (claimed > 0) { + require(pending == 0 && claimed == expected, "invalid yield provider implementation"); + _recordClaimed(claimed, expected); + } + } + + /// @notice A hook that is DELEGATE-CALLed by the Yield Manager for the provider + /// to perform any actions before the yield report process begins. + function preCommitYieldReportDelegateCallHook() external virtual onlyDelegateCall {} + + /// @notice Record a withdraw the stake balance of the provider. + /// @param amount Amount of staked balance to remove. + function _recordStakedWithdraw(uint256 amount) internal virtual { + stakedPrincipal -= amount; + emit Unstaked(id(), amount); + } + + /// @notice Record a pending balance to the provider. Needed only for providers + /// that use two-step withdrawals (e.g. Lido). + function _recordPending(uint256 amount) internal virtual { + pendingBalance += amount; + emit Pending(id(), amount); + } + + /// @notice Record a claimed balance to the provider. For providers with one-step + /// withdrawals, this method should be overriden to just emit the event + /// to avoid integer underflow. + function _recordClaimed(uint256 claimed, uint256 expected) internal virtual { + require(claimed <= expected, "invalid yield provider implementation"); + // Decrements pending balance by the expected amount, not the claimed amount. + // If claimed < expected, the difference (expected - claimed) must be considered + // as realized negative yield. To correctly reflect this, the difference is + // subtracted from the pending balance (and totalProviderValue). + pendingBalance -= expected; + emit Claimed(id(), claimed, expected); + } + + function _beforeCommitYield() internal virtual {} + + function _afterCommitYield() internal virtual {} +} diff --git a/contracts/src/yield/manager/universal/CrossDomainMessenger.sol b/contracts/src/yield/manager/universal/CrossDomainMessenger.sol new file mode 100644 index 00000000..ad86f948 --- /dev/null +++ b/contracts/src/yield/manager/universal/CrossDomainMessenger.sol @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity 0.8.15; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { SafeCall } from "../libraries/SafeCall.sol"; +import { Hashing } from "../libraries/Hashing.sol"; +import { Encoding } from "../libraries/Encoding.sol"; +import { Constants } from "../libraries/Constants.sol"; + +/// @custom:legacy +/// @title CrossDomainMessengerLegacySpacer0 +/// @notice Contract only exists to add a spacer to the CrossDomainMessenger where the +/// libAddressManager variable used to exist. Must be the first contract in the inheritance +/// tree of the CrossDomainMessenger. +contract CrossDomainMessengerLegacySpacer0 { + /// @custom:legacy + /// @custom:spacer libAddressManager + /// @notice Spacer for backwards compatibility. + address private spacer_0_0_20; +} + +/// @custom:legacy +/// @title CrossDomainMessengerLegacySpacer1 +/// @notice Contract only exists to add a spacer to the CrossDomainMessenger where the +/// PausableUpgradable and OwnableUpgradeable variables used to exist. Must be +/// the third contract in the inheritance tree of the CrossDomainMessenger. +contract CrossDomainMessengerLegacySpacer1 { + /// @custom:legacy + /// @custom:spacer ContextUpgradable's __gap + /// @notice Spacer for backwards compatibility. Comes from OpenZeppelin + /// ContextUpgradable. + uint256[50] private spacer_1_0_1600; + + /// @custom:legacy + /// @custom:spacer OwnableUpgradeable's _owner + /// @notice Spacer for backwards compatibility. + /// Come from OpenZeppelin OwnableUpgradeable. + address private spacer_51_0_20; + + /// @custom:legacy + /// @custom:spacer OwnableUpgradeable's __gap + /// @notice Spacer for backwards compatibility. Comes from OpenZeppelin + /// OwnableUpgradeable. + uint256[49] private spacer_52_0_1568; + + /// @custom:legacy + /// @custom:spacer PausableUpgradable's _paused + /// @notice Spacer for backwards compatibility. Comes from OpenZeppelin + /// PausableUpgradable. + bool private spacer_101_0_1; + + /// @custom:legacy + /// @custom:spacer PausableUpgradable's __gap + /// @notice Spacer for backwards compatibility. Comes from OpenZeppelin + /// PausableUpgradable. + uint256[49] private spacer_102_0_1568; + + /// @custom:legacy + /// @custom:spacer ReentrancyGuardUpgradeable's `_status` field. + /// @notice Spacer for backwards compatibility. + uint256 private spacer_151_0_32; + + /// @custom:legacy + /// @custom:spacer ReentrancyGuardUpgradeable's __gap + /// @notice Spacer for backwards compatibility. + uint256[49] private spacer_152_0_1568; + + /// @custom:legacy + /// @custom:spacer blockedMessages + /// @notice Spacer for backwards compatibility. + mapping(bytes32 => bool) private spacer_201_0_32; + + /// @custom:legacy + /// @custom:spacer relayedMessages + /// @notice Spacer for backwards compatibility. + mapping(bytes32 => bool) private spacer_202_0_32; +} + +/// @custom:upgradeable +/// @title CrossDomainMessenger +/// @notice CrossDomainMessenger is a base contract that provides the core logic for the L1 and L2 +/// cross-chain messenger contracts. It's designed to be a universal interface that only +/// needs to be extended slightly to provide low-level message passing functionality on each +/// chain it's deployed on. Currently only designed for message passing between two paired +/// chains and does not support one-to-many interactions. +/// Any changes to this contract MUST result in a semver bump for contracts that inherit it. +abstract contract CrossDomainMessenger is + CrossDomainMessengerLegacySpacer0, + Initializable, + CrossDomainMessengerLegacySpacer1 +{ + /// @notice Current message version identifier. + uint16 public constant MESSAGE_VERSION = 1; + + /// @notice Constant overhead added to the base gas for a message. + uint64 public constant RELAY_CONSTANT_OVERHEAD = 200_000; + + /// @notice Numerator for dynamic overhead added to the base gas for a message. + uint64 public constant MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR = 64; + + /// @notice Denominator for dynamic overhead added to the base gas for a message. + uint64 public constant MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR = 63; + + /// @notice Extra gas added to base gas for each byte of calldata in a message. + uint64 public constant MIN_GAS_CALLDATA_OVERHEAD = 16; + + /// @notice Gas reserved for performing the external call in `relayMessage`. + uint64 public constant RELAY_CALL_OVERHEAD = 40_000; + + /// @notice Gas reserved for finalizing the execution of `relayMessage` after the safe call. + uint64 public constant RELAY_RESERVED_GAS = 60_000; + + /// @notice Gas reserved for the execution between the `hasMinGas` check and the external + /// call in `relayMessage`. + uint64 public constant RELAY_GAS_CHECK_BUFFER = 5_000; + + /// @notice Address of the paired CrossDomainMessenger contract on the other chain. + address public immutable OTHER_MESSENGER; + + /// @notice Mapping of message hashes to boolean receipt values. Note that a message will only + /// be present in this mapping if it has successfully been relayed on this chain, and + /// can therefore not be relayed again. + mapping(bytes32 => bool) public successfulMessages; + + /// @notice Address of the sender of the currently executing message on the other chain. If the + /// value of this variable is the default value (0x00000000...dead) then no message is + /// currently being executed. Use the xDomainMessageSender getter which will throw an + /// error if this is the case. + address internal xDomainMsgSender; + + /// @notice Nonce for the next message to be sent, without the message version applied. Use the + /// messageNonce getter which will insert the message version into the nonce to give you + /// the actual nonce to be used for the message. + uint240 internal msgNonce; + + /// @notice Mapping of message hashes to a boolean if and only if the message has failed to be + /// executed at least once. A message will not be present in this mapping if it + /// successfully executed on the first attempt. + mapping(bytes32 => bool) public failedMessages; + + /// @notice Reserve extra slots in the storage layout for future upgrades. + /// A gap size of 42 was chosen here, so that the first slot used in a child contract + /// would be a multiple of 50. + uint256[42] private __gap; + + /// @notice Emitted whenever a message is sent to the other chain. + /// @param target Address of the recipient of the message. + /// @param sender Address of the sender of the message. + /// @param message Message to trigger the recipient address with. + /// @param messageNonce Unique nonce attached to the message. + /// @param gasLimit Minimum gas limit that the message can be executed with. + event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); + + /// @notice Additional event data to emit, required as of Bedrock. Cannot be merged with the + /// SentMessage event without breaking the ABI of this contract, this is good enough. + /// @param sender Address of the sender of the message. + /// @param value ETH value sent along with the message to the recipient. + event SentMessageExtension1(address indexed sender, uint256 value); + + /// @notice Emitted whenever a message is successfully relayed on this chain. + /// @param msgHash Hash of the message that was relayed. + event RelayedMessage(bytes32 indexed msgHash); + + /// @notice Emitted whenever a message fails to be relayed on this chain. + /// @param msgHash Hash of the message that failed to be relayed. + event FailedRelayedMessage(bytes32 indexed msgHash); + + /// @param _otherMessenger Address of the messenger on the paired chain. + constructor(address _otherMessenger) { + OTHER_MESSENGER = _otherMessenger; + } + + /// @notice Sends a message to some target address on the other chain. Note that if the call + /// always reverts, then the message will be unrelayable, and any ETH sent will be + /// permanently locked. The same will occur if the target on the other chain is + /// considered unsafe (see the _isUnsafeTarget() function). + /// @param _target Target contract or wallet address. + /// @param _message Message to trigger the target address with. + /// @param _minGasLimit Minimum gas limit that the message can be executed with. + function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable { + // Triggers a message to the other messenger. Note that the amount of gas provided to the + // message is the amount of gas requested by the user PLUS the base gas value. We want to + // guarantee the property that the call to the target contract will always have at least + // the minimum gas limit specified by the user. + _sendMessage( + OTHER_MESSENGER, + baseGas(_message, _minGasLimit), + msg.value, + abi.encodeWithSelector( + this.relayMessage.selector, + messageNonce(), + msg.sender, + _target, + msg.value, + _minGasLimit, + _message + ) + ); + + emit SentMessage(_target, msg.sender, _message, messageNonce(), _minGasLimit); + emit SentMessageExtension1(msg.sender, msg.value); + + unchecked { + ++msgNonce; + } + } + + /// @notice Relays a message that was sent by the other CrossDomainMessenger contract. Can only + /// be executed via cross-chain call from the other messenger OR if the message was + /// already received once and is currently being replayed. + /// @param _nonce Nonce of the message being relayed. + /// @param _sender Address of the user who sent the message. + /// @param _target Address that the message is targeted at. + /// @param _value ETH value to send with the message. + /// @param _minGasLimit Minimum amount of gas that the message can be executed with. + /// @param _message Message to send to the target. + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes calldata _message + ) external payable virtual { + (, uint16 version) = Encoding.decodeVersionedNonce(_nonce); + require(version < 2, "CrossDomainMessenger: only version 0 or 1 messages are supported at this time"); + + // If the message is version 0, then it's a migrated legacy withdrawal. We therefore need + // to check that the legacy version of the message has not already been relayed. + if (version == 0) { + bytes32 oldHash = Hashing.hashCrossDomainMessageV0(_target, _sender, _message, _nonce); + require(successfulMessages[oldHash] == false, "CrossDomainMessenger: legacy withdrawal already relayed"); + } + + // We use the v1 message hash as the unique identifier for the message because it commits + // to the value and minimum gas limit of the message. + bytes32 versionedHash = Hashing.hashCrossDomainMessageV1(_nonce, _sender, _target, _value, _minGasLimit, _message); + + if (_isOtherMessenger()) { + // These properties should always hold when the message is first submitted (as + // opposed to being replayed). + assert(msg.value == _value); + assert(!failedMessages[versionedHash]); + } else { + require(msg.value == 0, "CrossDomainMessenger: value must be zero unless message is from a system address"); + + require(failedMessages[versionedHash], "CrossDomainMessenger: message cannot be replayed"); + } + + require(_isUnsafeTarget(_target) == false, "CrossDomainMessenger: cannot send message to blocked system address"); + + require(successfulMessages[versionedHash] == false, "CrossDomainMessenger: message has already been relayed"); + + // If there is not enough gas left to perform the external call and finish the execution, + // return early and assign the message to the failedMessages mapping. + // We are asserting that we have enough gas to: + // 1. Call the target contract (_minGasLimit + RELAY_CALL_OVERHEAD + RELAY_GAS_CHECK_BUFFER) + // 1.a. The RELAY_CALL_OVERHEAD is included in `hasMinGas`. + // 2. Finish the execution after the external call (RELAY_RESERVED_GAS). + // + // If `xDomainMsgSender` is not the default L2 sender, this function + // is being re-entered. This marks the message as failed to allow it to be replayed. + if ( + !SafeCall.hasMinGas(_minGasLimit, RELAY_RESERVED_GAS + RELAY_GAS_CHECK_BUFFER) || + xDomainMsgSender != Constants.DEFAULT_L2_SENDER + ) { + failedMessages[versionedHash] = true; + emit FailedRelayedMessage(versionedHash); + + // Revert in this case if the transaction was triggered by the estimation address. This + // should only be possible during gas estimation or we have bigger problems. Reverting + // here will make the behavior of gas estimation change such that the gas limit + // computed will be the amount required to relay the message, even if that amount is + // greater than the minimum gas limit specified by the user. + if (tx.origin == Constants.ESTIMATION_ADDRESS) { + revert("CrossDomainMessenger: failed to relay message"); + } + + return; + } + + xDomainMsgSender = _sender; + bool success = SafeCall.call(_target, gasleft() - RELAY_RESERVED_GAS, _value, _message); + xDomainMsgSender = Constants.DEFAULT_L2_SENDER; + + if (success) { + // This check is identical to one above, but it ensures that the same message cannot be relayed + // twice, and adds a layer of protection against rentrancy. + assert(successfulMessages[versionedHash] == false); + successfulMessages[versionedHash] = true; + emit RelayedMessage(versionedHash); + } else { + failedMessages[versionedHash] = true; + emit FailedRelayedMessage(versionedHash); + + // Revert in this case if the transaction was triggered by the estimation address. This + // should only be possible during gas estimation or we have bigger problems. Reverting + // here will make the behavior of gas estimation change such that the gas limit + // computed will be the amount required to relay the message, even if that amount is + // greater than the minimum gas limit specified by the user. + if (tx.origin == Constants.ESTIMATION_ADDRESS) { + revert("CrossDomainMessenger: failed to relay message"); + } + } + } + + /// @notice Retrieves the address of the contract or wallet that initiated the currently + /// executing message on the other chain. Will throw an error if there is no message + /// currently being executed. Allows the recipient of a call to see who triggered it. + /// @return Address of the sender of the currently executing message on the other chain. + function xDomainMessageSender() external view returns (address) { + require(xDomainMsgSender != Constants.DEFAULT_L2_SENDER, "CrossDomainMessenger: xDomainMessageSender is not set"); + + return xDomainMsgSender; + } + + /// @notice Retrieves the next message nonce. Message version will be added to the upper two + /// bytes of the message nonce. Message version allows us to treat messages as having + /// different structures. + /// @return Nonce of the next message to be sent, with added message version. + function messageNonce() public view returns (uint256) { + return Encoding.encodeVersionedNonce(msgNonce, MESSAGE_VERSION); + } + + /// @notice Computes the amount of gas required to guarantee that a given message will be + /// received on the other chain without running out of gas. Guaranteeing that a message + /// will not run out of gas is important because this ensures that a message can always + /// be replayed on the other chain if it fails to execute completely. + /// @param _message Message to compute the amount of required gas for. + /// @param _minGasLimit Minimum desired gas limit when message goes to target. + /// @return Amount of gas required to guarantee message receipt. + function baseGas(bytes calldata _message, uint32 _minGasLimit) public pure returns (uint64) { + return + // Constant overhead + RELAY_CONSTANT_OVERHEAD + + // Calldata overhead + (uint64(_message.length) * MIN_GAS_CALLDATA_OVERHEAD) + + // Dynamic overhead (EIP-150) + ((_minGasLimit * MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR) / MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR) + + // Gas reserved for the worst-case cost of 3/5 of the `CALL` opcode's dynamic gas + // factors. (Conservative) + RELAY_CALL_OVERHEAD + + // Relay reserved gas (to ensure execution of `relayMessage` completes after the + // subcontext finishes executing) (Conservative) + RELAY_RESERVED_GAS + + // Gas reserved for the execution between the `hasMinGas` check and the `CALL` + // opcode. (Conservative) + RELAY_GAS_CHECK_BUFFER; + } + + /// @notice Initializer. + // solhint-disable-next-line func-name-mixedcase + function __CrossDomainMessenger_init() internal onlyInitializing { + // We only want to set the xDomainMsgSender to the default value if it hasn't been initialized yet, + // meaning that this is a fresh contract deployment. + // This prevents resetting the xDomainMsgSender to the default value during an upgrade, which would enable + // a reentrant withdrawal to sandwich the upgrade replay a withdrawal twice. + if (xDomainMsgSender == address(0)) { + xDomainMsgSender = Constants.DEFAULT_L2_SENDER; + } + } + + /// @notice Sends a low-level message to the other messenger. Needs to be implemented by child + /// contracts because the logic for this depends on the network where the messenger is + /// being deployed. + /// @param _to Recipient of the message on the other chain. + /// @param _gasLimit Minimum gas limit the message can be executed with. + /// @param _value Amount of ETH to send with the message. + /// @param _data Message data. + function _sendMessage(address _to, uint64 _gasLimit, uint256 _value, bytes memory _data) internal virtual; + + /// @notice Checks whether the message is coming from the other messenger. Implemented by child + /// contracts because the logic for this depends on the network where the messenger is + /// being deployed. + /// @return Whether the message is coming from the other messenger. + function _isOtherMessenger() internal view virtual returns (bool); + + /// @notice Checks whether a given call target is a system address that could cause the + /// messenger to peform an unsafe action. This is NOT a mechanism for blocking user + /// addresses. This is ONLY used to prevent the execution of messages to specific + /// system addresses that could cause security issues, e.g., having the + /// CrossDomainMessenger send messages to itself. + /// @param _target Address of the contract to check. + /// @return Whether or not the address is an unsafe system address. + function _isUnsafeTarget(address _target) internal view virtual returns (bool); +} diff --git a/contracts/src/yield/manager/universal/ISemver.sol b/contracts/src/yield/manager/universal/ISemver.sol new file mode 100644 index 00000000..8401c2f6 --- /dev/null +++ b/contracts/src/yield/manager/universal/ISemver.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +/// @title ISemver +/// @notice ISemver is a simple contract for ensuring that contracts are +/// versioned using semantic versioning. +interface ISemver { + /// @notice Getter for the semantic version of the contract. This is not + /// meant to be used onchain but instead meant to be used by offchain + /// tooling. + /// @return Semver contract version as a string. + function version() external view returns (string memory); +} diff --git a/contracts/src/yield/manager/universal/Semver.sol b/contracts/src/yield/manager/universal/Semver.sol new file mode 100644 index 00000000..ee9ba951 --- /dev/null +++ b/contracts/src/yield/manager/universal/Semver.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSL 1.1 - Copyright 2024 MetaLayer Labs Ltd. +pragma solidity ^0.8.0; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +/// @title Semver +/// @notice Semver is a simple contract for managing contract versions. +contract Semver { + /// @notice Contract version number (major). + uint256 private immutable MAJOR_VERSION; + + /// @notice Contract version number (minor). + uint256 private immutable MINOR_VERSION; + + /// @notice Contract version number (patch). + uint256 private immutable PATCH_VERSION; + + /// @param _major Version number (major). + /// @param _minor Version number (minor). + /// @param _patch Version number (patch). + constructor(uint256 _major, uint256 _minor, uint256 _patch) { + MAJOR_VERSION = _major; + MINOR_VERSION = _minor; + PATCH_VERSION = _patch; + } + + /// @notice Returns the full semver contract version. + /// @return Semver contract version as a string. + function version() public view returns (string memory) { + return + string( + abi.encodePacked( + Strings.toString(MAJOR_VERSION), + ".", + Strings.toString(MINOR_VERSION), + ".", + Strings.toString(PATCH_VERSION) + ) + ); + } +} diff --git a/contracts/src/yield/manager/vendor/AddressAliasHelper.sol b/contracts/src/yield/manager/vendor/AddressAliasHelper.sol new file mode 100644 index 00000000..46e9f3b6 --- /dev/null +++ b/contracts/src/yield/manager/vendor/AddressAliasHelper.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * Copyright 2019-2021, Offchain Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pragma solidity ^0.8.0; + +library AddressAliasHelper { + uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function that converts the address in the L1 that submitted a tx to + /// the inbox to the msg.sender viewed in the L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + unchecked { + l2Address = address(uint160(l1Address) + offset); + } + } + + /// @notice Utility function that converts the msg.sender viewed in the L2 to the + /// address in the L1 that submitted a tx to the inbox + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + unchecked { + l1Address = address(uint160(l2Address) - offset); + } + } +} diff --git a/contracts/src/yield/manager/vendor/solady/FixedPointMathLib.sol b/contracts/src/yield/manager/vendor/solady/FixedPointMathLib.sol new file mode 100644 index 00000000..e8aab99e --- /dev/null +++ b/contracts/src/yield/manager/vendor/solady/FixedPointMathLib.sol @@ -0,0 +1,1339 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +/// @notice Arithmetic library with operations for fixed-point numbers. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol) +/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) +library FixedPointMathLib { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The operation failed, as the output exceeds the maximum value of uint256. + error ExpOverflow(); + + /// @dev The operation failed, as the output exceeds the maximum value of uint256. + error FactorialOverflow(); + + /// @dev The operation failed, due to an overflow. + error RPowOverflow(); + + /// @dev The mantissa is too big to fit. + error MantissaOverflow(); + + /// @dev The operation failed, due to an multiplication overflow. + error MulWadFailed(); + + /// @dev The operation failed, due to an multiplication overflow. + error SMulWadFailed(); + + /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero. + error DivWadFailed(); + + /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero. + error SDivWadFailed(); + + /// @dev The operation failed, either due to a multiplication overflow, or a division by a zero. + error MulDivFailed(); + + /// @dev The division failed, as the denominator is zero. + error DivFailed(); + + /// @dev The full precision multiply-divide operation failed, either due + /// to the result being larger than 256 bits, or a division by a zero. + error FullMulDivFailed(); + + /// @dev The output is undefined, as the input is less-than-or-equal to zero. + error LnWadUndefined(); + + /// @dev The input outside the acceptable domain. + error OutOfDomain(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The scalar of ETH and most ERC20s. + uint256 internal constant WAD = 1e18; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* SIMPLIFIED FIXED POINT OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Equivalent to `(x * y) / WAD` rounded down. + function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`. + if gt(x, div(not(0), y)) { + if y { + mstore(0x00, 0xbac65e5b) // `MulWadFailed()`. + revert(0x1c, 0x04) + } + } + z := div(mul(x, y), WAD) + } + } + + /// @dev Equivalent to `(x * y) / WAD` rounded down. + function sMulWad(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(x, y) + // Equivalent to `require((x == 0 || z / x == y) && !(x == -1 && y == type(int256).min))`. + if iszero(gt(or(iszero(x), eq(sdiv(z, x), y)), lt(not(x), eq(y, shl(255, 1))))) { + mstore(0x00, 0xedcd4dd4) // `SMulWadFailed()`. + revert(0x1c, 0x04) + } + z := sdiv(z, WAD) + } + } + + /// @dev Equivalent to `(x * y) / WAD` rounded down, but without overflow checks. + function rawMulWad(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := div(mul(x, y), WAD) + } + } + + /// @dev Equivalent to `(x * y) / WAD` rounded down, but without overflow checks. + function rawSMulWad(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := sdiv(mul(x, y), WAD) + } + } + + /// @dev Equivalent to `(x * y) / WAD` rounded up. + function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(x, y) + // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`. + if iszero(eq(div(z, y), x)) { + if y { + mstore(0x00, 0xbac65e5b) // `MulWadFailed()`. + revert(0x1c, 0x04) + } + } + z := add(iszero(iszero(mod(z, WAD))), div(z, WAD)) + } + } + + /// @dev Equivalent to `(x * y) / WAD` rounded up, but without overflow checks. + function rawMulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD)) + } + } + + /// @dev Equivalent to `(x * WAD) / y` rounded down. + function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Equivalent to `require(y != 0 && x <= type(uint256).max / WAD)`. + if iszero(mul(y, lt(x, add(1, div(not(0), WAD))))) { + mstore(0x00, 0x7c5f487d) // `DivWadFailed()`. + revert(0x1c, 0x04) + } + z := div(mul(x, WAD), y) + } + } + + /// @dev Equivalent to `(x * WAD) / y` rounded down. + function sDivWad(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(x, WAD) + // Equivalent to `require(y != 0 && ((x * WAD) / WAD == x))`. + if iszero(mul(y, eq(sdiv(z, WAD), x))) { + mstore(0x00, 0x5c43740d) // `SDivWadFailed()`. + revert(0x1c, 0x04) + } + z := sdiv(z, y) + } + } + + /// @dev Equivalent to `(x * WAD) / y` rounded down, but without overflow and divide by zero checks. + function rawDivWad(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := div(mul(x, WAD), y) + } + } + + /// @dev Equivalent to `(x * WAD) / y` rounded down, but without overflow and divide by zero checks. + function rawSDivWad(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := sdiv(mul(x, WAD), y) + } + } + + /// @dev Equivalent to `(x * WAD) / y` rounded up. + function divWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Equivalent to `require(y != 0 && x <= type(uint256).max / WAD)`. + if iszero(mul(y, lt(x, add(1, div(not(0), WAD))))) { + mstore(0x00, 0x7c5f487d) // `DivWadFailed()`. + revert(0x1c, 0x04) + } + z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y)) + } + } + + /// @dev Equivalent to `(x * WAD) / y` rounded up, but without overflow and divide by zero checks. + function rawDivWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y)) + } + } + + /// @dev Equivalent to `x` to the power of `y`. + /// because `x ** y = (e ** ln(x)) ** y = e ** (ln(x) * y)`. + /// Note: This function is an approximation. + function powWad(int256 x, int256 y) internal pure returns (int256) { + // Using `ln(x)` means `x` must be greater than 0. + return expWad((lnWad(x) * y) / int256(WAD)); + } + + /// @dev Returns `exp(x)`, denominated in `WAD`. + /// Credit to Remco Bloemen under MIT license: https://2π.com/22/exp-ln + /// Note: This function is an approximation. Monotonically increasing. + function expWad(int256 x) internal pure returns (int256 r) { + unchecked { + // When the result is less than 0.5 we return zero. + // This happens when `x <= (log(1e-18) * 1e18) ~ -4.15e19`. + if (x <= -41446531673892822313) return r; + + /// @solidity memory-safe-assembly + assembly { + // When the result is greater than `(2**255 - 1) / 1e18` we can not represent it as + // an int. This happens when `x >= floor(log((2**255 - 1) / 1e18) * 1e18) ≈ 135`. + if iszero(slt(x, 135305999368893231589)) { + mstore(0x00, 0xa37bfec9) // `ExpOverflow()`. + revert(0x1c, 0x04) + } + } + + // `x` is now in the range `(-42, 136) * 1e18`. Convert to `(-42, 136) * 2**96` + // for more intermediate precision and a binary basis. This base conversion + // is a multiplication by 1e18 / 2**96 = 5**18 / 2**78. + x = (x << 78) / 5 ** 18; + + // Reduce range of x to (-½ ln 2, ½ ln 2) * 2**96 by factoring out powers + // of two such that exp(x) = exp(x') * 2**k, where k is an integer. + // Solving this gives k = round(x / log(2)) and x' = x - k * log(2). + int256 k = ((x << 96) / 54916777467707473351141471128 + 2 ** 95) >> 96; + x = x - k * 54916777467707473351141471128; + + // `k` is in the range `[-61, 195]`. + + // Evaluate using a (6, 7)-term rational approximation. + // `p` is made monic, we'll multiply by a scale factor later. + int256 y = x + 1346386616545796478920950773328; + y = ((y * x) >> 96) + 57155421227552351082224309758442; + int256 p = y + x - 94201549194550492254356042504812; + p = ((p * y) >> 96) + 28719021644029726153956944680412240; + p = p * x + (4385272521454847904659076985693276 << 96); + + // We leave `p` in `2**192` basis so we don't need to scale it back up for the division. + int256 q = x - 2855989394907223263936484059900; + q = ((q * x) >> 96) + 50020603652535783019961831881945; + q = ((q * x) >> 96) - 533845033583426703283633433725380; + q = ((q * x) >> 96) + 3604857256930695427073651918091429; + q = ((q * x) >> 96) - 14423608567350463180887372962807573; + q = ((q * x) >> 96) + 26449188498355588339934803723976023; + + /// @solidity memory-safe-assembly + assembly { + // Div in assembly because solidity adds a zero check despite the unchecked. + // The q polynomial won't have zeros in the domain as all its roots are complex. + // No scaling is necessary because p is already `2**96` too large. + r := sdiv(p, q) + } + + // r should be in the range `(0.09, 0.25) * 2**96`. + + // We now need to multiply r by: + // - The scale factor `s ≈ 6.031367120`. + // - The `2**k` factor from the range reduction. + // - The `1e18 / 2**96` factor for base conversion. + // We do this all at once, with an intermediate result in `2**213` + // basis, so the final right shift is always by a positive amount. + r = int256((uint256(r) * 3822833074963236453042738258902158003155416615667) >> uint256(195 - k)); + } + } + + /// @dev Returns `ln(x)`, denominated in `WAD`. + /// Credit to Remco Bloemen under MIT license: https://2π.com/22/exp-ln + /// Note: This function is an approximation. Monotonically increasing. + function lnWad(int256 x) internal pure returns (int256 r) { + /// @solidity memory-safe-assembly + assembly { + // We want to convert `x` from `10**18` fixed point to `2**96` fixed point. + // We do this by multiplying by `2**96 / 10**18`. But since + // `ln(x * C) = ln(x) + ln(C)`, we can simply do nothing here + // and add `ln(2**96 / 10**18)` at the end. + + // Compute `k = log2(x) - 96`, `r = 159 - k = 255 - log2(x) = 255 ^ log2(x)`. + r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) + r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) + r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) + r := or(r, shl(4, lt(0xffff, shr(r, x)))) + r := or(r, shl(3, lt(0xff, shr(r, x)))) + // We place the check here for more optimal stack operations. + if iszero(sgt(x, 0)) { + mstore(0x00, 0x1615e638) // `LnWadUndefined()`. + revert(0x1c, 0x04) + } + // forgefmt: disable-next-item + r := xor( + r, + byte( + and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)), + 0xf8f9f9faf9fdfafbf9fdfcfdfafbfcfef9fafdfafcfcfbfefafafcfbffffffff + ) + ) + + // Reduce range of x to (1, 2) * 2**96 + // ln(2^k * x) = k * ln(2) + ln(x) + x := shr(159, shl(r, x)) + + // Evaluate using a (8, 8)-term rational approximation. + // `p` is made monic, we will multiply by a scale factor later. + // forgefmt: disable-next-item + let p := sub( + // This heavily nested expression is to avoid stack-too-deep for via-ir. + sar( + 96, + mul( + add( + 43456485725739037958740375743393, + sar( + 96, + mul(add(24828157081833163892658089445524, sar(96, mul(add(3273285459638523848632254066296, x), x))), x) + ) + ), + x + ) + ), + 11111509109440967052023855526967 + ) + p := sub(sar(96, mul(p, x)), 45023709667254063763336534515857) + p := sub(sar(96, mul(p, x)), 14706773417378608786704636184526) + p := sub(mul(p, x), shl(96, 795164235651350426258249787498)) + // We leave `p` in `2**192` basis so we don't need to scale it back up for the division. + + // `q` is monic by convention. + let q := add(5573035233440673466300451813936, x) + q := add(71694874799317883764090561454958, sar(96, mul(x, q))) + q := add(283447036172924575727196451306956, sar(96, mul(x, q))) + q := add(401686690394027663651624208769553, sar(96, mul(x, q))) + q := add(204048457590392012362485061816622, sar(96, mul(x, q))) + q := add(31853899698501571402653359427138, sar(96, mul(x, q))) + q := add(909429971244387300277376558375, sar(96, mul(x, q))) + + // `p / q` is in the range `(0, 0.125) * 2**96`. + + // Finalization, we need to: + // - Multiply by the scale factor `s = 5.549…`. + // - Add `ln(2**96 / 10**18)`. + // - Add `k * ln(2)`. + // - Multiply by `10**18 / 2**96 = 5**18 >> 78`. + + // The q polynomial is known not to have zeros in the domain. + // No scaling required because p is already `2**96` too large. + p := sdiv(p, q) + // Multiply by the scaling factor: `s * 5**18 * 2**96`, base is now `5**18 * 2**192`. + p := mul(1677202110996718588342820967067443963516166, p) + // Add `ln(2) * k * 5**18 * 2**192`. + // forgefmt: disable-next-item + p := add(mul(16597577552685614221487285958193947469193820559219878177908093499208371, sub(159, r)), p) + // Add `ln(2**96 / 10**18) * 5**18 * 2**192`. + p := add(600920179829731861736702779321621459595472258049074101567377883020018308, p) + // Base conversion: mul `2**18 / 2**192`. + r := sar(174, p) + } + } + + /// @dev Returns `W_0(x)`, denominated in `WAD`. + /// See: https://en.wikipedia.org/wiki/Lambert_W_function + /// a.k.a. Product log function. This is an approximation of the principal branch. + /// Note: This function is an approximation. Monotonically increasing. + function lambertW0Wad(int256 x) internal pure returns (int256 w) { + // forgefmt: disable-next-item + unchecked { + if ((w = x) <= -367879441171442322) revert OutOfDomain(); // `x` less than `-1/e`. + (int256 wad, int256 p) = (int256(WAD), x); + uint256 c; // Whether we need to avoid catastrophic cancellation. + uint256 i = 4; // Number of iterations. + if (w <= 0x1ffffffffffff) { + if (-0x4000000000000 <= w) { + i = 1; // Inputs near zero only take one step to converge. + } else if (w <= -0x3ffffffffffffff) { + i = 32; // Inputs near `-1/e` take very long to converge. + } + } else if (uint256(w >> 63) == uint256(0)) { + /// @solidity memory-safe-assembly + assembly { + // Inline log2 for more performance, since the range is small. + let v := shr(49, w) + let l := shl(3, lt(0xff, v)) + l := add( + or( + l, + byte( + and(0x1f, shr(shr(l, v), 0x8421084210842108cc6318c6db6d54be)), + 0x0706060506020504060203020504030106050205030304010505030400000000 + ) + ), + 49 + ) + w := sdiv(shl(l, 7), byte(sub(l, 31), 0x0303030303030303040506080c13)) + c := gt(l, 60) + i := add(2, add(gt(l, 53), c)) + } + } else { + int256 ll = lnWad(w = lnWad(w)); + /// @solidity memory-safe-assembly + assembly { + // `w = ln(x) - ln(ln(x)) + b * ln(ln(x)) / ln(x)`. + w := add(sdiv(mul(ll, 1023715080943847266), w), sub(w, ll)) + i := add(3, iszero(shr(68, x))) + c := iszero(shr(143, x)) + } + if (c == uint256(0)) { + do { + // If `x` is big, use Newton's so that intermediate values won't overflow. + int256 e = expWad(w); + /// @solidity memory-safe-assembly + assembly { + let t := mul(w, div(e, wad)) + w := sub(w, sdiv(sub(t, x), div(add(e, t), wad))) + } + if (p <= w) break; + p = w; + } while (--i != uint256(0)); + /// @solidity memory-safe-assembly + assembly { + w := sub(w, sgt(w, 2)) + } + return w; + } + } + do { + // Otherwise, use Halley's for faster convergence. + int256 e = expWad(w); + /// @solidity memory-safe-assembly + assembly { + let t := add(w, wad) + let s := sub(mul(w, e), mul(x, wad)) + w := sub(w, sdiv(mul(s, wad), sub(mul(e, t), sdiv(mul(add(t, wad), s), add(t, t))))) + } + if (p <= w) break; + p = w; + } while (--i != c); + /// @solidity memory-safe-assembly + assembly { + w := sub(w, sgt(w, 2)) + } + // For certain ranges of `x`, we'll use the quadratic-rate recursive formula of + // R. Iacono and J.P. Boyd for the last iteration, to avoid catastrophic cancellation. + if (c == uint256(0)) return w; + int256 t = w | 1; + /// @solidity memory-safe-assembly + assembly { + x := sdiv(mul(x, wad), t) + } + x = (t * (wad + lnWad(x))); + /// @solidity memory-safe-assembly + assembly { + w := sdiv(x, add(wad, t)) + } + } + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* GENERAL NUMBER UTILITIES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns `a * b == x * y`, with full precision. + function fullMulEq(uint256 a, uint256 b, uint256 x, uint256 y) internal pure returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + result := and(eq(mul(a, b), mul(x, y)), eq(mulmod(x, y, not(0)), mulmod(a, b, not(0)))) + } + } + + /// @dev Calculates `floor(x * y / d)` with full precision. + /// Throws if result overflows a uint256 or when `d` is zero. + /// Credit to Remco Bloemen under MIT license: https://2π.com/21/muldiv + function fullMulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // 512-bit multiply `[p1 p0] = x * y`. + // Compute the product mod `2**256` and mod `2**256 - 1` + // then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 + // variables such that `product = p1 * 2**256 + p0`. + + // Temporarily use `z` as `p0` to save gas. + z := mul(x, y) // Lower 256 bits of `x * y`. + for { + + } 1 { + + } { + // If overflows. + if iszero(mul(or(iszero(x), eq(div(z, x), y)), d)) { + let mm := mulmod(x, y, not(0)) + let p1 := sub(mm, add(z, lt(mm, z))) // Upper 256 bits of `x * y`. + + /*------------------- 512 by 256 division --------------------*/ + + // Make division exact by subtracting the remainder from `[p1 p0]`. + let r := mulmod(x, y, d) // Compute remainder using mulmod. + let t := and(d, sub(0, d)) // The least significant bit of `d`. `t >= 1`. + // Make sure `z` is less than `2**256`. Also prevents `d == 0`. + // Placing the check here seems to give more optimal stack operations. + if iszero(gt(d, p1)) { + mstore(0x00, 0xae47f702) // `FullMulDivFailed()`. + revert(0x1c, 0x04) + } + d := div(d, t) // Divide `d` by `t`, which is a power of two. + // Invert `d mod 2**256` + // Now that `d` is an odd number, it has an inverse + // modulo `2**256` such that `d * inv = 1 mod 2**256`. + // Compute the inverse by starting with a seed that is correct + // correct for four bits. That is, `d * inv = 1 mod 2**4`. + let inv := xor(2, mul(3, d)) + // Now use 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. + inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**8 + inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**16 + inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**32 + inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**64 + inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**128 + z := mul( + // Divide [p1 p0] by the factors of two. + // Shift in bits from `p1` into `p0`. For this we need + // to flip `t` such that it is `2**256 / t`. + or(mul(sub(p1, gt(r, z)), add(div(sub(0, t), t), 1)), div(sub(z, r), t)), + mul(sub(2, mul(d, inv)), inv) // inverse mod 2**256 + ) + break + } + z := div(z, d) + break + } + } + } + + /// @dev Calculates `floor(x * y / d)` with full precision. + /// Behavior is undefined if `d` is zero or the final result cannot fit in 256 bits. + /// Performs the full 512 bit calculation regardless. + function fullMulDivUnchecked(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(x, y) + let mm := mulmod(x, y, not(0)) + let p1 := sub(mm, add(z, lt(mm, z))) + let t := and(d, sub(0, d)) + let r := mulmod(x, y, d) + d := div(d, t) + let inv := xor(2, mul(3, d)) + inv := mul(inv, sub(2, mul(d, inv))) + inv := mul(inv, sub(2, mul(d, inv))) + inv := mul(inv, sub(2, mul(d, inv))) + inv := mul(inv, sub(2, mul(d, inv))) + inv := mul(inv, sub(2, mul(d, inv))) + z := mul(or(mul(sub(p1, gt(r, z)), add(div(sub(0, t), t), 1)), div(sub(z, r), t)), mul(sub(2, mul(d, inv)), inv)) + } + } + + /// @dev Calculates `floor(x * y / d)` with full precision, rounded up. + /// Throws if result overflows a uint256 or when `d` is zero. + /// Credit to Uniswap-v3-core under MIT license: + /// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol + function fullMulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { + z = fullMulDiv(x, y, d); + /// @solidity memory-safe-assembly + assembly { + if mulmod(x, y, d) { + z := add(z, 1) + if iszero(z) { + mstore(0x00, 0xae47f702) // `FullMulDivFailed()`. + revert(0x1c, 0x04) + } + } + } + } + + /// @dev Calculates `floor(x * y / 2 ** n)` with full precision. + /// Throws if result overflows a uint256. + /// Credit to Philogy under MIT license: + /// https://github.com/SorellaLabs/angstrom/blob/main/contracts/src/libraries/X128MathLib.sol + function fullMulDivN(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // Temporarily use `z` as `p0` to save gas. + z := mul(x, y) // Lower 256 bits of `x * y`. We'll call this `z`. + for { + + } 1 { + + } { + if iszero(or(iszero(x), eq(div(z, x), y))) { + let k := and(n, 0xff) // `n`, cleaned. + let mm := mulmod(x, y, not(0)) + let p1 := sub(mm, add(z, lt(mm, z))) // Upper 256 bits of `x * y`. + // | p1 | z | + // Before: | p1_0 ¦ p1_1 | z_0 ¦ z_1 | + // Final: | 0 ¦ p1_0 | p1_1 ¦ z_0 | + // Check that final `z` doesn't overflow by checking that p1_0 = 0. + if iszero(shr(k, p1)) { + z := add(shl(sub(256, k), p1), shr(k, z)) + break + } + mstore(0x00, 0xae47f702) // `FullMulDivFailed()`. + revert(0x1c, 0x04) + } + z := shr(and(n, 0xff), z) + break + } + } + } + + /// @dev Returns `floor(x * y / d)`. + /// Reverts if `x * y` overflows, or `d` is zero. + function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(x, y) + // Equivalent to `require(d != 0 && (y == 0 || x <= type(uint256).max / y))`. + if iszero(mul(or(iszero(x), eq(div(z, x), y)), d)) { + mstore(0x00, 0xad251c27) // `MulDivFailed()`. + revert(0x1c, 0x04) + } + z := div(z, d) + } + } + + /// @dev Returns `ceil(x * y / d)`. + /// Reverts if `x * y` overflows, or `d` is zero. + function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(x, y) + // Equivalent to `require(d != 0 && (y == 0 || x <= type(uint256).max / y))`. + if iszero(mul(or(iszero(x), eq(div(z, x), y)), d)) { + mstore(0x00, 0xad251c27) // `MulDivFailed()`. + revert(0x1c, 0x04) + } + z := add(iszero(iszero(mod(z, d))), div(z, d)) + } + } + + /// @dev Returns `x`, the modular multiplicative inverse of `a`, such that `(a * x) % n == 1`. + function invMod(uint256 a, uint256 n) internal pure returns (uint256 x) { + /// @solidity memory-safe-assembly + assembly { + let g := n + let r := mod(a, n) + for { + let y := 1 + } 1 { + + } { + let q := div(g, r) + let t := g + g := r + r := sub(t, mul(r, q)) + let u := x + x := y + y := sub(u, mul(y, q)) + if iszero(r) { + break + } + } + x := mul(eq(g, 1), add(x, mul(slt(x, 0), n))) + } + } + + /// @dev Returns `ceil(x / d)`. + /// Reverts if `d` is zero. + function divUp(uint256 x, uint256 d) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + if iszero(d) { + mstore(0x00, 0x65244e4e) // `DivFailed()`. + revert(0x1c, 0x04) + } + z := add(iszero(iszero(mod(x, d))), div(x, d)) + } + } + + /// @dev Returns `max(0, x - y)`. Alias for `saturatingSub`. + function zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(gt(x, y), sub(x, y)) + } + } + + /// @dev Returns `max(0, x - y)`. + function saturatingSub(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(gt(x, y), sub(x, y)) + } + } + + /// @dev Returns `min(2 ** 256 - 1, x + y)`. + function saturatingAdd(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := or(sub(0, lt(add(x, y), x)), add(x, y)) + } + } + + /// @dev Returns `min(2 ** 256 - 1, x * y)`. + function saturatingMul(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := or(sub(or(iszero(x), eq(div(mul(x, y), x), y)), 1), mul(x, y)) + } + } + + /// @dev Returns `condition ? x : y`, without branching. + function ternary(bool condition, uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, y), iszero(condition))) + } + } + + /// @dev Returns `condition ? x : y`, without branching. + function ternary(bool condition, bytes32 x, bytes32 y) internal pure returns (bytes32 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, y), iszero(condition))) + } + } + + /// @dev Returns `condition ? x : y`, without branching. + function ternary(bool condition, address x, address y) internal pure returns (address z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, y), iszero(condition))) + } + } + + /// @dev Returns `x != 0 ? x : y`, without branching. + function coalesce(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := or(x, mul(y, iszero(x))) + } + } + + /// @dev Returns `x != bytes32(0) ? x : y`, without branching. + function coalesce(bytes32 x, bytes32 y) internal pure returns (bytes32 z) { + /// @solidity memory-safe-assembly + assembly { + z := or(x, mul(y, iszero(x))) + } + } + + /// @dev Returns `x != address(0) ? x : y`, without branching. + function coalesce(address x, address y) internal pure returns (address z) { + /// @solidity memory-safe-assembly + assembly { + z := or(x, mul(y, iszero(shl(96, x)))) + } + } + + /// @dev Exponentiate `x` to `y` by squaring, denominated in base `b`. + /// Reverts if the computation overflows. + function rpow(uint256 x, uint256 y, uint256 b) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mul(b, iszero(y)) // `0 ** 0 = 1`. Otherwise, `0 ** n = 0`. + if x { + z := xor(b, mul(xor(b, x), and(y, 1))) // `z = isEven(y) ? scale : x` + let half := shr(1, b) // Divide `b` by 2. + // Divide `y` by 2 every iteration. + for { + y := shr(1, y) + } y { + y := shr(1, y) + } { + let xx := mul(x, x) // Store x squared. + let xxRound := add(xx, half) // Round to the nearest number. + // Revert if `xx + half` overflowed, or if `x ** 2` overflows. + if or(lt(xxRound, xx), shr(128, x)) { + mstore(0x00, 0x49f7642b) // `RPowOverflow()`. + revert(0x1c, 0x04) + } + x := div(xxRound, b) // Set `x` to scaled `xxRound`. + // If `y` is odd: + if and(y, 1) { + let zx := mul(z, x) // Compute `z * x`. + let zxRound := add(zx, half) // Round to the nearest number. + // If `z * x` overflowed or `zx + half` overflowed: + if or(xor(div(zx, x), z), lt(zxRound, zx)) { + // Revert if `x` is non-zero. + if x { + mstore(0x00, 0x49f7642b) // `RPowOverflow()`. + revert(0x1c, 0x04) + } + } + z := div(zxRound, b) // Return properly scaled `zxRound`. + } + } + } + } + } + + /// @dev Returns the square root of `x`, rounded down. + function sqrt(uint256 x) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + // `floor(sqrt(2**15)) = 181`. `sqrt(2**15) - 181 = 2.84`. + z := 181 // The "correct" value is 1, but this saves a multiplication later. + + // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad + // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. + + // Let `y = x / 2**r`. We check `y >= 2**(k + 8)` + // but shift right by `k` bits to ensure that if `x >= 256`, then `y >= 256`. + let r := shl(7, lt(0xffffffffffffffffffffffffffffffffff, x)) + r := or(r, shl(6, lt(0xffffffffffffffffff, shr(r, x)))) + r := or(r, shl(5, lt(0xffffffffff, shr(r, x)))) + r := or(r, shl(4, lt(0xffffff, shr(r, x)))) + z := shl(shr(1, r), z) + + // Goal was to get `z*z*y` within a small factor of `x`. More iterations could + // get y in a tighter range. Currently, we will have y in `[256, 256*(2**16))`. + // We ensured `y >= 256` so that the relative difference between `y` and `y+1` is small. + // That's not possible if `x < 256` but we can just verify those cases exhaustively. + + // Now, `z*z*y <= x < z*z*(y+1)`, and `y <= 2**(16+8)`, and either `y >= 256`, or `x < 256`. + // Correctness can be checked exhaustively for `x < 256`, so we assume `y >= 256`. + // Then `z*sqrt(y)` is within `sqrt(257)/sqrt(256)` of `sqrt(x)`, or about 20bps. + + // For `s` in the range `[1/256, 256]`, the estimate `f(s) = (181/1024) * (s+1)` + // is in the range `(1/2.84 * sqrt(s), 2.84 * sqrt(s))`, + // with largest error when `s = 1` and when `s = 256` or `1/256`. + + // Since `y` is in `[256, 256*(2**16))`, let `a = y/65536`, so that `a` is in `[1/256, 256)`. + // Then we can estimate `sqrt(y)` using + // `sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2**18`. + + // There is no overflow risk here since `y < 2**136` after the first branch above. + z := shr(18, mul(z, add(shr(r, x), 65536))) // A `mul()` is saved from starting `z` at 181. + + // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + // If `x+1` is a perfect square, the Babylonian method cycles between + // `floor(sqrt(x))` and `ceil(sqrt(x))`. This statement ensures we return floor. + // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division + z := sub(z, lt(div(x, z), z)) + } + } + + /// @dev Returns the cube root of `x`, rounded down. + /// Credit to bout3fiddy and pcaversaccio under AGPLv3 license: + /// https://github.com/pcaversaccio/snekmate/blob/main/src/snekmate/utils/math.vy + /// Formally verified by xuwinnie: + /// https://github.com/vectorized/solady/blob/main/audits/xuwinnie-solady-cbrt-proof.pdf + function cbrt(uint256 x) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + let r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) + r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) + r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) + r := or(r, shl(4, lt(0xffff, shr(r, x)))) + r := or(r, shl(3, lt(0xff, shr(r, x)))) + // Makeshift lookup table to nudge the approximate log2 result. + z := div(shl(div(r, 3), shl(lt(0xf, shr(r, x)), 0xf)), xor(7, mod(r, 3))) + // Newton-Raphson's. + z := div(add(add(div(x, mul(z, z)), z), z), 3) + z := div(add(add(div(x, mul(z, z)), z), z), 3) + z := div(add(add(div(x, mul(z, z)), z), z), 3) + z := div(add(add(div(x, mul(z, z)), z), z), 3) + z := div(add(add(div(x, mul(z, z)), z), z), 3) + z := div(add(add(div(x, mul(z, z)), z), z), 3) + z := div(add(add(div(x, mul(z, z)), z), z), 3) + // Round down. + z := sub(z, lt(div(x, mul(z, z)), z)) + } + } + + /// @dev Returns the square root of `x`, denominated in `WAD`, rounded down. + function sqrtWad(uint256 x) internal pure returns (uint256 z) { + unchecked { + if (x <= type(uint256).max / 10 ** 18) return sqrt(x * 10 ** 18); + z = (1 + sqrt(x)) * 10 ** 9; + z = (fullMulDivUnchecked(x, 10 ** 18, z) + z) >> 1; + } + /// @solidity memory-safe-assembly + assembly { + z := sub(z, gt(999999999999999999, sub(mulmod(z, z, x), 1))) // Round down. + } + } + + /// @dev Returns the cube root of `x`, denominated in `WAD`, rounded down. + /// Formally verified by xuwinnie: + /// https://github.com/vectorized/solady/blob/main/audits/xuwinnie-solady-cbrt-proof.pdf + function cbrtWad(uint256 x) internal pure returns (uint256 z) { + unchecked { + if (x <= type(uint256).max / 10 ** 36) return cbrt(x * 10 ** 36); + z = (1 + cbrt(x)) * 10 ** 12; + z = (fullMulDivUnchecked(x, 10 ** 36, z * z) + z + z) / 3; + } + /// @solidity memory-safe-assembly + assembly { + let p := x + for { + + } 1 { + + } { + if iszero(shr(229, p)) { + if iszero(shr(199, p)) { + p := mul(p, 100000000000000000) // 10 ** 17. + break + } + p := mul(p, 100000000) // 10 ** 8. + break + } + if iszero(shr(249, p)) { + p := mul(p, 100) + } + break + } + let t := mulmod(mul(z, z), z, p) + z := sub(z, gt(lt(t, shr(1, p)), iszero(t))) // Round down. + } + } + + /// @dev Returns the factorial of `x`. + function factorial(uint256 x) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := 1 + if iszero(lt(x, 58)) { + mstore(0x00, 0xaba0f2a2) // `FactorialOverflow()`. + revert(0x1c, 0x04) + } + for { + + } x { + x := sub(x, 1) + } { + z := mul(z, x) + } + } + } + + /// @dev Returns the log2 of `x`. + /// Equivalent to computing the index of the most significant bit (MSB) of `x`. + /// Returns 0 if `x` is zero. + function log2(uint256 x) internal pure returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) + r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) + r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) + r := or(r, shl(4, lt(0xffff, shr(r, x)))) + r := or(r, shl(3, lt(0xff, shr(r, x)))) + // forgefmt: disable-next-item + r := or( + r, + byte( + and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)), + 0x0706060506020504060203020504030106050205030304010505030400000000 + ) + ) + } + } + + /// @dev Returns the log2 of `x`, rounded up. + /// Returns 0 if `x` is zero. + function log2Up(uint256 x) internal pure returns (uint256 r) { + r = log2(x); + /// @solidity memory-safe-assembly + assembly { + r := add(r, lt(shl(r, 1), x)) + } + } + + /// @dev Returns the log10 of `x`. + /// Returns 0 if `x` is zero. + function log10(uint256 x) internal pure returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + if iszero(lt(x, 100000000000000000000000000000000000000)) { + x := div(x, 100000000000000000000000000000000000000) + r := 38 + } + if iszero(lt(x, 100000000000000000000)) { + x := div(x, 100000000000000000000) + r := add(r, 20) + } + if iszero(lt(x, 10000000000)) { + x := div(x, 10000000000) + r := add(r, 10) + } + if iszero(lt(x, 100000)) { + x := div(x, 100000) + r := add(r, 5) + } + r := add(r, add(gt(x, 9), add(gt(x, 99), add(gt(x, 999), gt(x, 9999))))) + } + } + + /// @dev Returns the log10 of `x`, rounded up. + /// Returns 0 if `x` is zero. + function log10Up(uint256 x) internal pure returns (uint256 r) { + r = log10(x); + /// @solidity memory-safe-assembly + assembly { + r := add(r, lt(exp(10, r), x)) + } + } + + /// @dev Returns the log256 of `x`. + /// Returns 0 if `x` is zero. + function log256(uint256 x) internal pure returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) + r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) + r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) + r := or(r, shl(4, lt(0xffff, shr(r, x)))) + r := or(shr(3, r), lt(0xff, shr(r, x))) + } + } + + /// @dev Returns the log256 of `x`, rounded up. + /// Returns 0 if `x` is zero. + function log256Up(uint256 x) internal pure returns (uint256 r) { + r = log256(x); + /// @solidity memory-safe-assembly + assembly { + r := add(r, lt(shl(shl(3, r), 1), x)) + } + } + + /// @dev Returns the scientific notation format `mantissa * 10 ** exponent` of `x`. + /// Useful for compressing prices (e.g. using 25 bit mantissa and 7 bit exponent). + function sci(uint256 x) internal pure returns (uint256 mantissa, uint256 exponent) { + /// @solidity memory-safe-assembly + assembly { + mantissa := x + if mantissa { + if iszero(mod(mantissa, 1000000000000000000000000000000000)) { + mantissa := div(mantissa, 1000000000000000000000000000000000) + exponent := 33 + } + if iszero(mod(mantissa, 10000000000000000000)) { + mantissa := div(mantissa, 10000000000000000000) + exponent := add(exponent, 19) + } + if iszero(mod(mantissa, 1000000000000)) { + mantissa := div(mantissa, 1000000000000) + exponent := add(exponent, 12) + } + if iszero(mod(mantissa, 1000000)) { + mantissa := div(mantissa, 1000000) + exponent := add(exponent, 6) + } + if iszero(mod(mantissa, 10000)) { + mantissa := div(mantissa, 10000) + exponent := add(exponent, 4) + } + if iszero(mod(mantissa, 100)) { + mantissa := div(mantissa, 100) + exponent := add(exponent, 2) + } + if iszero(mod(mantissa, 10)) { + mantissa := div(mantissa, 10) + exponent := add(exponent, 1) + } + } + } + } + + /// @dev Convenience function for packing `x` into a smaller number using `sci`. + /// The `mantissa` will be in bits [7..255] (the upper 249 bits). + /// The `exponent` will be in bits [0..6] (the lower 7 bits). + /// Use `SafeCastLib` to safely ensure that the `packed` number is small + /// enough to fit in the desired unsigned integer type: + /// ``` + /// uint32 packed = SafeCastLib.toUint32(FixedPointMathLib.packSci(777 ether)); + /// ``` + function packSci(uint256 x) internal pure returns (uint256 packed) { + (x, packed) = sci(x); // Reuse for `mantissa` and `exponent`. + /// @solidity memory-safe-assembly + assembly { + if shr(249, x) { + mstore(0x00, 0xce30380c) // `MantissaOverflow()`. + revert(0x1c, 0x04) + } + packed := or(shl(7, x), packed) + } + } + + /// @dev Convenience function for unpacking a packed number from `packSci`. + function unpackSci(uint256 packed) internal pure returns (uint256 unpacked) { + unchecked { + unpacked = (packed >> 7) * 10 ** (packed & 0x7f); + } + } + + /// @dev Returns the average of `x` and `y`. Rounds towards zero. + function avg(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = (x & y) + ((x ^ y) >> 1); + } + } + + /// @dev Returns the average of `x` and `y`. Rounds towards negative infinity. + function avg(int256 x, int256 y) internal pure returns (int256 z) { + unchecked { + z = (x >> 1) + (y >> 1) + (x & y & 1); + } + } + + /// @dev Returns the absolute value of `x`. + function abs(int256 x) internal pure returns (uint256 z) { + unchecked { + z = (uint256(x) + uint256(x >> 255)) ^ uint256(x >> 255); + } + } + + /// @dev Returns the absolute distance between `x` and `y`. + function dist(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := add(xor(sub(0, gt(x, y)), sub(y, x)), gt(x, y)) + } + } + + /// @dev Returns the absolute distance between `x` and `y`. + function dist(int256 x, int256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := add(xor(sub(0, sgt(x, y)), sub(y, x)), sgt(x, y)) + } + } + + /// @dev Returns the minimum of `x` and `y`. + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, y), lt(y, x))) + } + } + + /// @dev Returns the minimum of `x` and `y`. + function min(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, y), slt(y, x))) + } + } + + /// @dev Returns the maximum of `x` and `y`. + function max(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, y), gt(y, x))) + } + } + + /// @dev Returns the maximum of `x` and `y`. + function max(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, y), sgt(y, x))) + } + } + + /// @dev Returns `x`, bounded to `minValue` and `maxValue`. + function clamp(uint256 x, uint256 minValue, uint256 maxValue) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, minValue), gt(minValue, x))) + z := xor(z, mul(xor(z, maxValue), lt(maxValue, z))) + } + } + + /// @dev Returns `x`, bounded to `minValue` and `maxValue`. + function clamp(int256 x, int256 minValue, int256 maxValue) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := xor(x, mul(xor(x, minValue), sgt(minValue, x))) + z := xor(z, mul(xor(z, maxValue), slt(maxValue, z))) + } + } + + /// @dev Returns greatest common divisor of `x` and `y`. + function gcd(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + for { + z := x + } y { + + } { + let t := y + y := mod(z, y) + z := t + } + } + } + + /// @dev Returns `a + (b - a) * (t - begin) / (end - begin)`, + /// with `t` clamped between `begin` and `end` (inclusive). + /// Agnostic to the order of (`a`, `b`) and (`end`, `begin`). + /// If `begins == end`, returns `t <= begin ? a : b`. + function lerp(uint256 a, uint256 b, uint256 t, uint256 begin, uint256 end) internal pure returns (uint256) { + if (begin > end) (t, begin, end) = (~t, ~begin, ~end); + if (t <= begin) return a; + if (t >= end) return b; + unchecked { + if (b >= a) return a + fullMulDiv(b - a, t - begin, end - begin); + return a - fullMulDiv(a - b, t - begin, end - begin); + } + } + + /// @dev Returns `a + (b - a) * (t - begin) / (end - begin)`. + /// with `t` clamped between `begin` and `end` (inclusive). + /// Agnostic to the order of (`a`, `b`) and (`end`, `begin`). + /// If `begins == end`, returns `t <= begin ? a : b`. + function lerp(int256 a, int256 b, int256 t, int256 begin, int256 end) internal pure returns (int256) { + if (begin > end) (t, begin, end) = (~t, ~begin, ~end); + if (t <= begin) return a; + if (t >= end) return b; + // forgefmt: disable-next-item + unchecked { + if (b >= a) return int256(uint256(a) + fullMulDiv(uint256(b - a), uint256(t - begin), uint256(end - begin))); + return int256(uint256(a) - fullMulDiv(uint256(a - b), uint256(t - begin), uint256(end - begin))); + } + } + + /// @dev Returns if `x` is an even number. Some people may need this. + function isEven(uint256 x) internal pure returns (bool) { + return x & uint256(1) == uint256(0); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* RAW NUMBER OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns `x + y`, without checking for overflow. + function rawAdd(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = x + y; + } + } + + /// @dev Returns `x + y`, without checking for overflow. + function rawAdd(int256 x, int256 y) internal pure returns (int256 z) { + unchecked { + z = x + y; + } + } + + /// @dev Returns `x - y`, without checking for underflow. + function rawSub(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = x - y; + } + } + + /// @dev Returns `x - y`, without checking for underflow. + function rawSub(int256 x, int256 y) internal pure returns (int256 z) { + unchecked { + z = x - y; + } + } + + /// @dev Returns `x * y`, without checking for overflow. + function rawMul(uint256 x, uint256 y) internal pure returns (uint256 z) { + unchecked { + z = x * y; + } + } + + /// @dev Returns `x * y`, without checking for overflow. + function rawMul(int256 x, int256 y) internal pure returns (int256 z) { + unchecked { + z = x * y; + } + } + + /// @dev Returns `x / y`, returning 0 if `y` is zero. + function rawDiv(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := div(x, y) + } + } + + /// @dev Returns `x / y`, returning 0 if `y` is zero. + function rawSDiv(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := sdiv(x, y) + } + } + + /// @dev Returns `x % y`, returning 0 if `y` is zero. + function rawMod(uint256 x, uint256 y) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mod(x, y) + } + } + + /// @dev Returns `x % y`, returning 0 if `y` is zero. + function rawSMod(int256 x, int256 y) internal pure returns (int256 z) { + /// @solidity memory-safe-assembly + assembly { + z := smod(x, y) + } + } + + /// @dev Returns `(x + y) % d`, return 0 if `d` if zero. + function rawAddMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := addmod(x, y, d) + } + } + + /// @dev Returns `(x * y) % d`, return 0 if `d` if zero. + function rawMulMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) { + /// @solidity memory-safe-assembly + assembly { + z := mulmod(x, y, d) + } + } +} diff --git a/contracts/src/yield/manager/vendor/solmate/tokens/ERC20.sol b/contracts/src/yield/manager/vendor/solmate/tokens/ERC20.sol new file mode 100644 index 00000000..3cdbb31c --- /dev/null +++ b/contracts/src/yield/manager/vendor/solmate/tokens/ERC20.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.15; + +/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) +/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. +abstract contract ERC20 { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /*////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + uint8 public immutable decimals; + + /*////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + /*////////////////////////////////////////////////////////////// + EIP-2612 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 internal immutable INITIAL_CHAIN_ID; + + bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; + + mapping(address => uint256) public nonces; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + } + + /*////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 amount) public virtual returns (bool) { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) public virtual returns (bool) { + balanceOf[msg.sender] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } + + /*////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); + + allowance[recoveredAddress][spender] = value; + } + + emit Approval(owner, spender, value); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } +} diff --git a/contracts/src/yield/manager/vendor/solmate/utils/SafeTransferLib.sol b/contracts/src/yield/manager/vendor/solmate/utils/SafeTransferLib.sol new file mode 100644 index 00000000..7a2938e6 --- /dev/null +++ b/contracts/src/yield/manager/vendor/solmate/utils/SafeTransferLib.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.15; + +import { ERC20 } from "../tokens/ERC20.sol"; + +/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol) +/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. +library SafeTransferLib { + /*////////////////////////////////////////////////////////////// + ETH OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function safeTransferETH(address to, uint256 amount) internal { + bool success; + + /// @solidity memory-safe-assembly + assembly { + // Transfer the ETH and store if it succeeded or not. + success := call(gas(), to, amount, 0, 0, 0, 0) + } + + require(success, "ETH_TRANSFER_FAILED"); + } + + /*////////////////////////////////////////////////////////////// + ERC20 OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function safeTransferFrom(ERC20 token, address from, address to, uint256 amount) internal { + bool success; + + /// @solidity memory-safe-assembly + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) + mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument. + mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. + mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. + + // We use 100 because the length of our calldata totals up like so: 4 + 32 * 3. + // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. + success := call(gas(), token, 0, freeMemoryPointer, 100, 0, 32) + + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data and token has code. + if and(iszero(and(eq(mload(0), 1), gt(returndatasize(), 31))), success) { + success := iszero(or(iszero(extcodesize(token)), returndatasize())) + } + } + + require(success, "TRANSFER_FROM_FAILED"); + } + + function safeTransfer(ERC20 token, address to, uint256 amount) internal { + bool success; + + /// @solidity memory-safe-assembly + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) + mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. + + // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. + // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. + success := call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) + + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data and token has code. + if and(iszero(and(eq(mload(0), 1), gt(returndatasize(), 31))), success) { + success := iszero(or(iszero(extcodesize(token)), returndatasize())) + } + } + + require(success, "TRANSFER_FAILED"); + } + + function safeApprove(ERC20 token, address to, uint256 amount) internal { + bool success; + + /// @solidity memory-safe-assembly + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000) + mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. + + // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. + // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. + success := call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) + + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data and token has code. + if and(iszero(and(eq(mload(0), 1), gt(returndatasize(), 31))), success) { + success := iszero(or(iszero(extcodesize(token)), returndatasize())) + } + } + + require(success, "APPROVE_FAILED"); + } +}