Description:
Proxy contract enabling upgradeable smart contract patterns. Delegates calls to an implementation contract.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/modules/bridge/ArbitrumBridgeModule.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
// Modules
import { BridgeModuleBase } from "@modules/bridge/base/BridgeModuleBase.sol";
// Interfaces
import { IArbitrumBridgeModule } from "@modules/bridge/interfaces/arbitrum/IArbitrumBridgeModule.sol";
import { IModule } from "@modules/interfaces/IModule.sol";
import { IL1GatewayRouter } from "@modules/bridge/interfaces/arbitrum/IL1GatewayRouter.sol";
import { IInbox } from "@modules/bridge/interfaces/arbitrum/IInbox.sol";
// Libraries
import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol";
import { ERC20UtilsLib } from "@modules/libraries/ERC20UtilsLib.sol";
import { OrderHashLib } from "@modules/libraries/OrderHashLib.sol";
import { BridgeLib } from "@modules/libraries/BridgeLib.sol";
// Types
import { Order } from "@types/Order.sol";
/// @title ArbitrumBridgeModule
/// @notice Module for bridging tokens from Ethereum to Arbitrum via the canonical Arbitrum bridge
/// @dev Supports both ERC20 tokens (via L1GatewayRouter) and ETH (via Inbox)
contract ArbitrumBridgeModule is BridgeModuleBase, IArbitrumBridgeModule {
using SafeTransferLib for address;
using ERC20UtilsLib for address;
using OrderHashLib for Order;
/*//////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
/// @notice The L1 Gateway Router address for Arbitrum ERC20 bridging
IL1GatewayRouter public immutable L1_GATEWAY_ROUTER;
/// @notice The Inbox address for Arbitrum ETH bridging
IInbox public immutable INBOX;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/// @notice Constructs the ArbitrumBridgeModule
/// @param _portikusV2 The PortikusV2 contract address
/// @param l1GatewayRouter The L1 Gateway Router address for ERC20 bridging
/// @param inbox The Inbox address for ETH bridging
constructor(
address _portikusV2,
address l1GatewayRouter,
address inbox
)
BridgeModuleBase("Arbitrum Bridge Module", "1.0.0", _portikusV2)
{
L1_GATEWAY_ROUTER = IL1GatewayRouter(l1GatewayRouter);
INBOX = IInbox(inbox);
}
/*//////////////////////////////////////////////////////////////
EXTERNAL
//////////////////////////////////////////////////////////////*/
/// @notice Bridges tokens using Arbitrum's native bridge (L1GatewayRouter for ERC20, Inbox for ETH)
/// @dev This is the unique function signature for Arbitrum bridging
/// @param order The order containing bridge details
/// @param beneficiary The address of the beneficiary on Arbitrum (L2)
/// @param inputAmount The amount of tokens to bridge
/// @param bridgeData Encoded (uint256) or (uint256, uint256) representing the agent fee and settlement (protocol +
/// partner) fees
/// @return nativeFee Returns the actual native fee paid to the bridge protocol
function bridgeWithArbitrum(
Order memory order,
address beneficiary,
uint256 inputAmount,
bytes memory bridgeData
)
external
payable
returns (uint256 nativeFee)
{
// Require bridge access authorization from settlement module
BridgeLib.requireBridgeInitiated();
return _bridgeInternal(order, beneficiary, inputAmount, bridgeData);
}
/// @inheritdoc IModule
function selectors() external pure override returns (bytes4[] memory moduleSelectors) {
moduleSelectors = new bytes4[](1);
moduleSelectors[0] = this.bridgeWithArbitrum.selector;
}
/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// @notice Internal bridge implementation for Arbitrum
/// @param order The order containing bridge details
/// @param beneficiary The address of the beneficiary on Arbitrum (L2)
/// @param inputAmount The amount of input tokens to bridge
/// @param bridgeData Encoded (uint256, uint256) representing the agent fee and settlement (protocol & partner) fees
function _bridgeInternal(
Order memory order,
address beneficiary,
uint256 inputAmount,
bytes memory bridgeData
)
internal
returns (uint256 nativeFee)
{
ArbitrumBridgeData memory protocolData = _validateAndDecodeProtocolData(order);
// Decode bridge data and calculate capped agent fee
uint256 agentFee = _calculateCappedFee(abi.decode(bridgeData, (uint256)), protocolData.maxAgentFee);
uint256 bridgeAmount = inputAmount - agentFee;
// Compensate agent in dest tokens for paying ETH gas fees
if (agentFee > 0) order.destToken.transferTo(msg.sender, agentFee);
// Calculate ETH value needed for gas fees
nativeFee = _calculateGasFees(protocolData);
// Handle ETH vs ERC20 bridging based on source token (what we're bridging)
if (order.destToken.isEth()) {
// Pass agentFee and settlementEthFees to ensure msg.value accounting and refunds are correct under
// delegatecall
_bridgeETH(beneficiary, bridgeAmount, protocolData, nativeFee);
} else {
_bridgeERC20(order.destToken, beneficiary, bridgeAmount, protocolData, nativeFee);
}
// Emit the bridge event with destination-decimal output amount
emit TokensBridged(
order.hash(),
beneficiary,
order.bridge.destinationChainId,
inputAmount,
_convertToDestDecimals(bridgeAmount, order.bridge.scalingFactor),
"arbitrum"
);
}
/// @notice Bridge ERC20 tokens via the L1 Gateway Router
/// @dev Gas fees are paid from msg.value provided by the agent
/// @param l1Token The L1 token address to bridge
/// @param beneficiary The recipient address on Arbitrum
/// @param amount The amount to bridge (after agent fee)
/// @param protocolData The Arbitrum bridge data
/// @param gasFees The gas fees required for the bridge operation
function _bridgeERC20(
address l1Token,
address beneficiary,
uint256 amount,
ArbitrumBridgeData memory protocolData,
uint256 gasFees
)
internal
{
// Get the gateway for this token
address gateway = L1_GATEWAY_ROUTER.getGateway(l1Token);
if (gateway == address(0)) revert NoGatewayFound();
// Approve the gateway to spend the tokens
l1Token.safeApprove(gateway, amount);
// Encode submission cost and empty data for the user
bytes memory userEncodedData = abi.encode(protocolData.maxSubmissionCost, protocolData.extraData);
L1_GATEWAY_ROUTER.outboundTransferCustomRefund{ value: gasFees }(
l1Token, beneficiary, beneficiary, amount, protocolData.maxGas, protocolData.gasPriceBid, userEncodedData
);
}
/// @notice Bridge ETH to Arbitrum via Inbox using retryable tickets
/// @dev Uses Arbitrum's Inbox.createRetryableTicket() for ETH bridging with custom recipient
/// @dev Agent provides ETH for both bridge amount and gas fees via msg.value
/// @param beneficiary The recipient address on Arbitrum L2
/// @param amount The amount of ETH to bridge to the beneficiary
/// @param protocolData The Arbitrum bridge data containing gas parameters
/// @param gasFees The gas fees required for the bridge operation
function _bridgeETH(
address beneficiary,
uint256 amount,
ArbitrumBridgeData memory protocolData,
uint256 gasFees
)
internal
{
// Only forward the exact value required by the Arbitrum Inbox (amount + gasFees)
INBOX.createRetryableTicket{ value: amount + gasFees }(
beneficiary, // to: destination address on L2
amount, // l2CallValue: ETH amount to send to beneficiary
protocolData.maxSubmissionCost, // maxSubmissionCost: base fee for submission
beneficiary, // excessFeeRefundAddress: beneficiary gets gas refunds
beneficiary, // callValueRefundAddress: beneficiary gets ETH if ticket times out
protocolData.maxGas, // gasLimit: max gas for L2 execution
protocolData.gasPriceBid, // maxFeePerGas: gas price bid
"" // data: empty for simple ETH transfer
);
}
/// @notice Validates and decodes the protocol data from the order
/// @param order The order containing the protocol data to validate
/// @return protocolData The validated and decoded Arbitrum bridge data
function _validateAndDecodeProtocolData(Order memory order)
internal
pure
returns (ArbitrumBridgeData memory protocolData)
{
// Validate protocol data exists
if (order.bridge.protocolData.length == 0) revert MissingProtocolData();
// Decode protocol data
protocolData = abi.decode(order.bridge.protocolData, (ArbitrumBridgeData));
// Validate gas parameters based on token type
uint256 gasLimit = order.destToken.isEth() ? 21_000 : 200_000; // 21k for ETH, 200k for ERC20
if (protocolData.maxGas < gasLimit) revert InsufficientMaxGas();
if (protocolData.gasPriceBid < 1e7) revert InvalidGasPriceBid(); // Min 0.01 gwei
}
/// @notice Calculates total gas fees required for Arbitrum retryable tickets
/// @param protocolData The Arbitrum bridge data containing gas parameters
/// @return gasFees The total gas fees (submission cost + execution gas cost)
function _calculateGasFees(ArbitrumBridgeData memory protocolData) internal pure returns (uint256 gasFees) {
return protocolData.maxSubmissionCost + (protocolData.maxGas * protocolData.gasPriceBid);
}
}
"
},
"src/modules/bridge/base/BridgeModuleBase.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
// Contracts
import { BaseModule } from "@modules/base/BaseModule.sol";
// Libraries
import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol";
import { ERC20UtilsLib } from "@modules/libraries/ERC20UtilsLib.sol";
/// @title Bridge Module Base
/// @notice Abstract base contract for bridge modules containing shared functionality
/// @dev Provides common patterns, validations, and utilities for bridge implementations
abstract contract BridgeModuleBase is BaseModule {
using SafeTransferLib for address;
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
/// @notice Thrown when protocol data is missing or empty
error MissingProtocolData();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when tokens are bridged across chains
/// @param orderHash The hash of the order being bridged
/// @param beneficiary The recipient on the destination chain
/// @param destChainId The destination chain ID
/// @param inputAmount The amount of tokens being bridged (expressed in source token decimals)
/// @param outputAmount The expected amount on the destination chain, expressed in destination token decimals
/// after applying `_convertToDestDecimals`
/// @param bridgeProtocol The bridge protocol used (e.g., "optimism", "polygon", "cctp")
event TokensBridged(
bytes32 indexed orderHash,
address indexed beneficiary,
uint256 indexed destChainId,
uint256 inputAmount,
uint256 outputAmount,
string bridgeProtocol
);
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/// @notice Constructor for BridgeModuleBase
/// @param _name The module name
/// @param _version The module version
/// @param _portikusV2 The PortikusV2 contract address
constructor(
string memory _name,
string memory _version,
address _portikusV2
)
BaseModule(_name, _version, _portikusV2)
{ }
/*//////////////////////////////////////////////////////////////
SHARED UTILITIES
//////////////////////////////////////////////////////////////*/
/// @notice Calculates agent fee with cap protection
/// @param requestedFee The requested agent fee
/// @param maxFee The maximum allowed fee
/// @return The capped agent fee
function _calculateCappedFee(uint256 requestedFee, uint256 maxFee) internal pure returns (uint256) {
return requestedFee > maxFee ? maxFee : requestedFee;
}
/*//////////////////////////////////////////////////////////////
DECIMAL CONVERSION
//////////////////////////////////////////////////////////////*/
/// @notice Converts an amount from source token decimals to destination token decimals
/// @dev scalingFactor = destDecimals - srcDecimals
///
/// Behavior:
/// - scalingFactor > 0 (dest has more decimals than src): multiply by 10^scalingFactor
/// - scalingFactor < 0 (dest has fewer decimals than src): divide by 10^(-scalingFactor)
///
/// Constraints:
/// - |scalingFactor| <= 18 to avoid 10^N overflow and keep gas predictable
///
/// Examples:
/// - USDC (6) -> WETH (18): scalingFactor = +12; 1e6 -> 1e6 * 10^12 = 1e18
/// - WETH (18) -> USDC (6): scalingFactor = -12; 1e18 -> 1e18 / 10^12 = 1e6
///
/// @param amount Amount expressed in source token decimals
/// @param scalingFactor Signed decimal difference (destDecimals - srcDecimals)
/// @return Amount expressed in destination token decimals
function _convertToDestDecimals(uint256 amount, int8 scalingFactor) internal pure returns (uint256) {
if (scalingFactor == 0) return amount;
// Validate scaling factor to prevent overflow
int8 absFactor = scalingFactor >= 0 ? scalingFactor : -scalingFactor;
require(uint8(absFactor) <= 18, "Bridge: scaling factor too large");
uint256 scale = 10 ** uint8(absFactor);
if (scalingFactor > 0) {
// destDecimals > srcDecimals: multiply to convert src -> dest
return amount * scale;
} else {
// srcDecimals > destDecimals: divide to convert src -> dest
return amount / scale;
}
}
}
"
},
"src/modules/bridge/interfaces/arbitrum/IArbitrumBridgeModule.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
// Types
import { Order } from "@types/Order.sol";
/// @title Arbitrum Bridge Module Interface
/// @notice Interface for Arbitrum's native L1GatewayRouter bridge protocol
/// @dev Supports both ERC20 tokens (via L1GatewayRouter) and ETH (via Inbox)
interface IArbitrumBridgeModule {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
/// @notice Thrown when max gas is too low for L2 execution (21k for ETH, 200k for ERC20)
error InsufficientMaxGas();
/// @notice Thrown when gas price bid is too low (minimum 0.01 gwei)
error InvalidGasPriceBid();
/// @notice Thrown when no gateway is found for a token
error NoGatewayFound();
/// @notice Thrown when insufficient ETH provided for gas fees (ERC20 bridging)
error InsufficientEthForGasFees();
/// @notice Thrown when insufficient ETH provided for bridge amount and gas fees (ETH bridging)
error InsufficientEthForBridgeAndGasFees();
/*//////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/
/// @notice Protocol-controlled Arbitrum bridge configuration
/// @dev This struct defines the data encoded in order.bridge.protocolData by the backend
/// These parameters cannot be manipulated by agents during execution
struct ArbitrumBridgeData {
/// @dev Max gas deducted from user's L2 balance to cover execution
uint256 maxGas;
/// @dev Gas price for L2 execution
uint256 gasPriceBid;
/// @dev Max gas deducted for base submission fee
uint256 maxSubmissionCost;
/// @dev Optional extra data for L2 execution
bytes extraData;
/// @dev The maximum fee in order dest token that agent can set as compensation for paying for the bridge
uint256 maxAgentFee;
}
/*//////////////////////////////////////////////////////////////
EXTERNAL
//////////////////////////////////////////////////////////////*/
/// @notice Bridges tokens using Arbitrum's native bridge infrastructure
/// @dev For ERC20: Uses L1GatewayRouter, agent provides ETH for gas via msg.value
/// @dev For ETH: Uses Inbox.createRetryableTicket, agent provides ETH for bridge amount + gas via msg.value
/// @param order The order containing bridge details and protocol data
/// @param beneficiary The address of the beneficiary on Arbitrum (L2)
/// @param inputAmount The amount of tokens to bridge
/// @param bridgeData Encoded uint256 representing the agent fee for gas compensation
/// @return nativeFee Returns the actual native fee paid to the bridge protocol
function bridgeWithArbitrum(
Order memory order,
address beneficiary,
uint256 inputAmount,
bytes memory bridgeData
)
external
payable
returns (uint256 nativeFee);
}
"
},
"src/modules/interfaces/IModule.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
/// @title IModule
/// @notice Core interfaces that all modules must implement to be compatible with the Portikus protocol
interface IModule {
/*//////////////////////////////////////////////////////////////
METADATA
//////////////////////////////////////////////////////////////*/
/// @notice Returns the name of the module
function name() external view returns (string memory);
/// @notice Returns the version of the module
function version() external view returns (string memory);
/*//////////////////////////////////////////////////////////////
SELECTORS
//////////////////////////////////////////////////////////////*/
/// @notice Used by the executor to determine which functions should be installed
/// @dev The implementation should not include any of the function selectors defined in the IModule interface itself
/// @return moduleSelectors An array of function selectors that the module implements
function selectors() external pure returns (bytes4[] memory moduleSelectors);
}
"
},
"src/modules/bridge/interfaces/arbitrum/IL1GatewayRouter.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
/// @title Interface for Arbitrum's L1GatewayRouter
/// @notice Defines the API for routing token deposits to appropriate gateways
interface IL1GatewayRouter {
/// @notice Deposit ERC20 token from Ethereum into Arbitrum using the registered or otherwise default gateway
/// @dev Some legacy gateway might not have the outboundTransferCustomRefund method and will revert, in such case
/// use outboundTransfer instead
/// L2 address alias will not be applied to the following types of addresses on L1:
/// - an externally-owned account
/// - a contract in construction
/// - an address where a contract will be created
/// - an address where a contract lived, but was destroyed
/// @param _token L1 address of ERC20
/// @param _refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2
/// @param _to Account to be credited with the tokens in the L2 (can be the user's L2 account or a contract), not
/// subject to L2 aliasing
/// This account, or its L2 alias if it have code in L1, will also be able to cancel the retryable
/// ticket and receive callvalue refund
/// @param _amount Token Amount
/// @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution
/// @param _gasPriceBid Gas price for L2 execution
/// @param _data encoded data from router and user
/// @return res abi encoded inbox sequence number
function outboundTransferCustomRefund(
address _token,
address _refundTo,
address _to,
uint256 _amount,
uint256 _maxGas,
uint256 _gasPriceBid,
bytes calldata _data
)
external
payable
returns (bytes memory res);
/// @notice Gets the gateway contract for a given token
/// @dev Returns the token-specific gateway if registered, otherwise returns the default gateway.
/// Returns address(0) if the gateway is disabled or not a valid contract.
/// @param _token L1 address of the token
/// @return gateway Address of the gateway contract for this token, or address(0) if no valid gateway exists
function getGateway(address _token) external view returns (address gateway);
}
"
},
"src/modules/bridge/interfaces/arbitrum/IInbox.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
/// @title Interface for Arbitrum's Inbox contract
/// @notice Used for depositing ETH from L1 to L2 Arbitrum
interface IInbox {
/// @notice Deposit eth from L1 to L2 to address of the sender if sender is an EOA, and to its aliased address if
/// the sender is a contract
/// @dev This does not trigger the fallback function when receiving in the L2 side.
/// Look into retryable tickets if you are interested in this functionality.
/// @dev This function should not be called inside contract constructors
/// @return The unique message number of the deposit
function depositEth() external payable returns (uint256);
/// @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts
/// @dev all msg.value will deposited to callValueRefundAddress on L2
/// @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error
/// @param to destination L2 contract address
/// @param l2CallValue call value for retryable L2 message
/// @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee
/// @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance. In case
/// this address is a contract, funds will be received in its alias on L2.
/// @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled.
/// In case this address is a contract, funds will be received in its alias on L2.
/// @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic
/// value used to trigger the RetryableData error)
/// @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the
/// RetryableData error)
/// @param data ABI encoded data of L2 message
/// @return unique message number of the retryable transaction
function createRetryableTicket(
address to,
uint256 l2CallValue,
uint256 maxSubmissionCost,
address excessFeeRefundAddress,
address callValueRefundAddress,
uint256 gasLimit,
uint256 maxFeePerGas,
bytes calldata data
)
external
payable
returns (uint256);
}
"
},
"lib/solady/src/utils/SafeTransferLib.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeTransferLib.sol)
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @author Permit2 operations from (https://github.com/Uniswap/permit2/blob/main/src/libraries/Permit2Lib.sol)
///
/// @dev Note:
/// - For ETH transfers, please use `forceSafeTransferETH` for DoS protection.
library SafeTransferLib {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The ETH transfer has failed.
error ETHTransferFailed();
/// @dev The ERC20 `transferFrom` has failed.
error TransferFromFailed();
/// @dev The ERC20 `transfer` has failed.
error TransferFailed();
/// @dev The ERC20 `approve` has failed.
error ApproveFailed();
/// @dev The Permit2 operation has failed.
error Permit2Failed();
/// @dev The Permit2 amount must be less than `2**160 - 1`.
error Permit2AmountOverflow();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Suggested gas stipend for contract receiving ETH that disallows any storage writes.
uint256 internal constant GAS_STIPEND_NO_STORAGE_WRITES = 2300;
/// @dev Suggested gas stipend for contract receiving ETH to perform a few
/// storage reads and writes, but low enough to prevent griefing.
uint256 internal constant GAS_STIPEND_NO_GRIEF = 100000;
/// @dev The unique EIP-712 domain domain separator for the DAI token contract.
bytes32 internal constant DAI_DOMAIN_SEPARATOR =
0xdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7;
/// @dev The address for the WETH9 contract on Ethereum mainnet.
address internal constant WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
/// @dev The canonical Permit2 address.
/// [Github](https://github.com/Uniswap/permit2)
/// [Etherscan](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3)
address internal constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ETH OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
// If the ETH transfer MUST succeed with a reasonable gas budget, use the force variants.
//
// The regular variants:
// - Forwards all remaining gas to the target.
// - Reverts if the target reverts.
// - Reverts if the current contract has insufficient balance.
//
// The force variants:
// - Forwards with an optional gas stipend
// (defaults to `GAS_STIPEND_NO_GRIEF`, which is sufficient for most cases).
// - If the target reverts, or if the gas stipend is exhausted,
// creates a temporary contract to force send the ETH via `SELFDESTRUCT`.
// Future compatible with `SENDALL`: https://eips.ethereum.org/EIPS/eip-4758.
// - Reverts if the current contract has insufficient balance.
//
// The try variants:
// - Forwards with a mandatory gas stipend.
// - Instead of reverting, returns whether the transfer succeeded.
/// @dev Sends `amount` (in wei) ETH to `to`.
function safeTransferETH(address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gas(), to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Sends all the ETH in the current contract to `to`.
function safeTransferAllETH(address to) internal {
/// @solidity memory-safe-assembly
assembly {
// Transfer all the ETH and check if it succeeded or not.
if iszero(call(gas(), to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
}
}
/// @dev Force sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
function forceSafeTransferETH(address to, uint256 amount, uint256 gasStipend) internal {
/// @solidity memory-safe-assembly
assembly {
if lt(selfbalance(), amount) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
if iszero(call(gasStipend, to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends all the ETH in the current contract to `to`, with a `gasStipend`.
function forceSafeTransferAllETH(address to, uint256 gasStipend) internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(call(gasStipend, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends `amount` (in wei) ETH to `to`, with `GAS_STIPEND_NO_GRIEF`.
function forceSafeTransferETH(address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
if lt(selfbalance(), amount) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
if iszero(call(GAS_STIPEND_NO_GRIEF, to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(amount, 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Force sends all the ETH in the current contract to `to`, with `GAS_STIPEND_NO_GRIEF`.
function forceSafeTransferAllETH(address to) internal {
/// @solidity memory-safe-assembly
assembly {
// forgefmt: disable-next-item
if iszero(call(GAS_STIPEND_NO_GRIEF, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, to) // Store the address in scratch space.
mstore8(0x0b, 0x73) // Opcode `PUSH20`.
mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.
if iszero(create(selfbalance(), 0x0b, 0x16)) { revert(codesize(), codesize()) } // For gas estimation.
}
}
}
/// @dev Sends `amount` (in wei) ETH to `to`, with a `gasStipend`.
function trySafeTransferETH(address to, uint256 amount, uint256 gasStipend)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
success := call(gasStipend, to, amount, codesize(), 0x00, codesize(), 0x00)
}
}
/// @dev Sends all the ETH in the current contract to `to`, with a `gasStipend`.
function trySafeTransferAllETH(address to, uint256 gasStipend)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
success := call(gasStipend, to, selfbalance(), codesize(), 0x00, codesize(), 0x00)
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERC20 OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
/// Reverts upon failure.
///
/// The `from` account must have at least `amount` approved for
/// the current contract to manage.
function safeTransferFrom(address token, address from, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, amount) // Store the `amount` argument.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x23b872dd000000000000000000000000) // `transferFrom(address,address,uint256)`.
let success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
///
/// The `from` account must have at least `amount` approved for the current contract to manage.
function trySafeTransferFrom(address token, address from, address to, uint256 amount)
internal
returns (bool success)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, amount) // Store the `amount` argument.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x23b872dd000000000000000000000000) // `transferFrom(address,address,uint256)`.
success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
success := lt(or(iszero(extcodesize(token)), returndatasize()), success)
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends all of ERC20 `token` from `from` to `to`.
/// Reverts upon failure.
///
/// The `from` account must have their entire balance approved for the current contract to manage.
function safeTransferAllFrom(address token, address from, address to)
internal
returns (uint256 amount)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x40, to) // Store the `to` argument.
mstore(0x2c, shl(96, from)) // Store the `from` argument.
mstore(0x0c, 0x70a08231000000000000000000000000) // `balanceOf(address)`.
// Read the balance, reverting upon failure.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x1c, 0x24, 0x60, 0x20)
)
) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
mstore(0x00, 0x23b872dd) // `transferFrom(address,address,uint256)`.
amount := mload(0x60) // The `amount` is already at 0x60. We'll need to return it.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x7939f424) // `TransferFromFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x60, 0) // Restore the zero slot to zero.
mstore(0x40, m) // Restore the free memory pointer.
}
}
/// @dev Sends `amount` of ERC20 `token` from the current contract to `to`.
/// Reverts upon failure.
function safeTransfer(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0xa9059cbb000000000000000000000000) // `transfer(address,uint256)`.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sends all of ERC20 `token` from the current contract to `to`.
/// Reverts upon failure.
function safeTransferAll(address token, address to) internal returns (uint256 amount) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0x70a08231) // Store the function selector of `balanceOf(address)`.
mstore(0x20, address()) // Store the address of the current contract.
// Read the balance, reverting upon failure.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x1c, 0x24, 0x34, 0x20)
)
) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
mstore(0x14, to) // Store the `to` argument.
amount := mload(0x34) // The `amount` is already at 0x34. We'll need to return it.
mstore(0x00, 0xa9059cbb000000000000000000000000) // `transfer(address,uint256)`.
// Perform the transfer, reverting upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x90b8ec18) // `TransferFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract.
/// Reverts upon failure.
function safeApprove(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x3e3f8f73) // `ApproveFailed()`.
revert(0x1c, 0x04)
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract.
/// If the initial attempt to approve fails, attempts to reset the approved amount to zero,
/// then retries the approval again (some tokens, e.g. USDT, requires this).
/// Reverts upon failure.
function safeApproveWithRetry(address token, address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
// Perform the approval, retrying upon failure.
let success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x34, 0) // Store 0 for the `amount`.
mstore(0x00, 0x095ea7b3000000000000000000000000) // `approve(address,uint256)`.
pop(call(gas(), token, 0, 0x10, 0x44, codesize(), 0x00)) // Reset the approval.
mstore(0x34, amount) // Store back the original `amount`.
// Retry the approval, reverting upon failure.
success := call(gas(), token, 0, 0x10, 0x44, 0x00, 0x20)
if iszero(and(eq(mload(0x00), 1), success)) {
// Check the `extcodesize` again just in case the token selfdestructs lol.
if iszero(lt(or(iszero(extcodesize(token)), returndatasize()), success)) {
mstore(0x00, 0x3e3f8f73) // `ApproveFailed()`.
revert(0x1c, 0x04)
}
}
}
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/// @dev Returns the amount of ERC20 `token` owned by `account`.
/// Returns zero if the `token` does not exist.
function balanceOf(address token, address account) internal view returns (uint256 amount) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, account) // Store the `account` argument.
mstore(0x00, 0x70a08231000000000000000000000000) // `balanceOf(address)`.
amount :=
mul( // The arguments of `mul` are evaluated from right to left.
mload(0x20),
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), token, 0x10, 0x24, 0x20, 0x20)
)
)
}
}
/// @dev Sends `amount` of ERC20 `token` from `from` to `to`.
/// If the initial attempt fails, try to use Permit2 to transfer the token.
/// Reverts upon failure.
///
/// The `from` account must have at least `amount` approved for the current contract to manage.
function safeTransferFrom2(address token, address from, address to, uint256 amount) internal {
if (!trySafeTransferFrom(token, from, to, amount)) {
permit2TransferFrom(token, from, to, amount);
}
}
/// @dev Sends `amount` of ERC20 `token` from `from` to `to` via Permit2.
/// Reverts upon failure.
function permit2TransferFrom(address token, address from, address to, uint256 amount)
internal
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40)
mstore(add(m, 0x74), shr(96, shl(96, token)))
mstore(add(m, 0x54), amount)
mstore(add(m, 0x34), to)
mstore(add(m, 0x20), shl(96, from))
// `transferFrom(address,address,uint160,address)`.
mstore(m, 0x36c78516000000000000000000000000)
let p := PERMIT2
let exists := eq(chainid(), 1)
if iszero(exists) { exists := iszero(iszero(extcodesize(p))) }
if iszero(
and(
call(gas(), p, 0, add(m, 0x10), 0x84, codesize(), 0x00),
lt(iszero(extcodesize(token)), exists) // Token has code and Permit2 exists.
)
) {
mstore(0x00, 0x7939f4248757f0fd) // `TransferFromFailed()` or `Permit2AmountOverflow()`.
revert(add(0x18, shl(2, iszero(iszero(shr(160, amount))))), 0x04)
}
}
}
/// @dev Permit a user to spend a given amount of
/// another user's tokens via native EIP-2612 permit if possible, falling
/// back to Permit2 if native permit fails or is not implemented on the token.
function permit2(
address token,
address owner,
address spender,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
for {} shl(96, xor(token, WETH9)) {} {
mstore(0x00, 0x3644e515) // `DOMAIN_SEPARATOR()`.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
lt(iszero(mload(0x00)), eq(returndatasize(), 0x20)), // Returns 1 non-zero word.
// Gas stipend to limit gas burn for tokens that don't refund gas when
// an non-existing function is called. 5K should be enough for a SLOAD.
staticcall(5000, token, 0x1c, 0x04, 0x00, 0x20)
)
) { break }
// After here, we can be sure that token is a contract.
let m := mload(0x40)
mstore(add(m, 0x34), spender)
mstore(add(m, 0x20), shl(96, owner))
mstore(add(m, 0x74), deadline)
if eq(mload(0x00), DAI_DOMAIN_SEPARATOR) {
mstore(0x14, owner)
mstore(0x00, 0x7ecebe00000000000000000000000000) // `nonces(address)`.
mstore(add(m, 0x94), staticcall(gas(), token, 0x10, 0x24, add(m, 0x54), 0x20))
mstore(m, 0x8fcbaf0c000000000000000000000000) // `IDAIPermit.permit`.
// `nonces` is already at `add(m, 0x54)`.
// `1` is already stored at `add(m, 0x94)`.
mstore(add(m, 0xb4), and(0xff, v))
mstore(add(m, 0xd4), r)
mstore(add(m, 0xf4), s)
success := call(gas(), token, 0, add(m, 0x10), 0x104, codesize(), 0x00)
break
}
mstore(m, 0xd505accf000000000000000000000000) // `IERC20Permit.permit`.
mstore(add(m, 0x54), amount)
mstore(add(m, 0x94), and(0xff, v))
mstore(add(m, 0xb4), r)
mstore(add(m, 0xd4), s)
success := call(gas(), token, 0, add(m, 0x10), 0xe4, codesize(), 0x00)
break
}
}
if (!success) simplePermit2(token, owner, spender, amount, deadline, v, r, s);
}
/// @dev Simple permit on the Permit2 contract.
function simplePermit2(
address token,
address owner,
address spender,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40)
mstore(m, 0x927da105) // `allowance(address,address,address)`.
{
let addressMask := shr(96, not(0))
mstore(add(m, 0x20), and(addressMask, owner))
mstore(add(m, 0x40), and(addressMask, token))
mstore(add(m, 0x60), and(addressMask, spender))
mstore(add(m, 0xc0), and(addressMask, spender))
}
let p := mul(PERMIT2, iszero(shr(160, amount)))
if iszero(
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x5f), // Returns 3 words: `amount`, `expiration`, `nonce`.
staticcall(gas(), p, add(m, 0x1c), 0x64, add(m, 0x60), 0x60)
)
) {
mstore(0x00, 0x6b836e6b8757f0fd) // `Permit2Failed()` or `Permit2AmountOverflow()`.
revert(add(0x18, shl(2, iszero(p))), 0x04)
}
mstore(m, 0x2b67b570) // `Permit2.permit` (PermitSingle variant).
// `owner` is already `add(m, 0x20)`.
// `token` is already at `add(m, 0x40)`.
mstore(add(m, 0x60), amount)
mstore(add(m, 0x80), 0xffffffffffff) // `expiration = type(uint48).max`.
// `nonce` is already at `add(m, 0xa0)`.
// `spender` is already at `add(m, 0xc0)`.
mstore(add(m, 0xe0), deadline)
mstore(add(m, 0x100), 0x100) // `signature` offset.
mstore(add(m, 0x120), 0x41) // `signature` length.
mstore(add(m, 0x140), r)
mstore(add(m, 0x160), s)
mstore(add(m, 0x180), shl(248, v))
if iszero( // Revert if token does not have code, or if the call fails.
mul(extcodesize(token), call(gas(), p, 0, add(m, 0x1c), 0x184, codesize(), 0x00))) {
mstore(0x00, 0x6b836e6b) // `Permit2Failed()`.
revert(0x1c, 0x04)
}
}
}
}
"
},
"src/modules/libraries/ERC20UtilsLib.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
// Libraries
import { SafeTransferLib } from "@solady/utils/SafeTransferLib.sol";
/// @title ERC20 Utility Library
/// @dev Library with common functions used by different modules within the Portikus V2 protocol
library ERC20UtilsLib {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when the permit execution fails
error PermitFailed();
/*//////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
/// @dev An address used to represent the native token
address internal constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
/// @dev The address of the Permit2 contract
address internal constant PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
/*//////////////////////////////////////////////////////////////
LIBRARIES
//////////////////////////////////////////////////////////////*/
using SafeTransferLib for address;
/*//////////////////////////////////////////////////////////////
TRANSFER
//////////////////////////////////////////////////////////////*/
/// @dev Transfer the dest token to the order beneficiary, reducing the amount by 1 wei for gas optimization
/// purposes. If the destToken is ETH, it will transfer native ETH to recipient
/// @param destToken The address of the dest token
/// @param recipient The address to transfer to
/// @param amount The amount to transfer
function transferTo(address destToken, address recipient, uint256 amount) internal {
// If the destToken is ETH, transfer native ETH
if (isEth(destToken)) {
recipient.safeTransferETH(amount);
} else {
// Otherwise, transfer the dest token to the recipient, reducing the amount by 1 wei
// for gas optimization purposes
destToken.safeTransfer(recipient, amount);
}
}
/// @dev Transfer the src token from the user to a recipient, using permit2 allowance or erc20 allowance based on
/// permit length, if the permit length is 192 or 1 it will call transferFrom using permit2
/// allowance, otherwise it will call transferFrom using erc20 allowance unless permit length is 96
/// @param srcToken The address of the src token
/// @param owner The owner of the token
/// @param recipient The recipient of the token
/// @param amount The amount to transfer
/// @param permitLength The length of the permit
function transferFrom(
address srcToken,
address owner,
address recipient,
uint256 amount,
uint256 permitLength
)
internal
{
// Skip transferring if the permit length is 96 (permit2TransferFrom)
if (permitLength == 96) {
return;
}
// If the permit length is 192 or 1 execute permit2TransferFrom to transfer the
// input assets from the owner to the agent, otherwise execute ERC20 transferFrom
if (permitLength == 192 || permitLength == 1) {
// Transfer the input assets from the owner to the recipient using permit2 allowance
srcToken.permit2TransferFrom(owner, recipient, amount);
} else {
// Transfer the input assets from the owner to the recipient using the ERC20 transferFrom function
srcToken.safeTransferFrom(owner, recipient, amount);
}
}
/*//////////////////////////////////////////////////////////////
PERMIT
//////////////////////////////////////////////////////////////*/
/// @dev Executes the permit function on the provided token, depending on the permit length,
/// it will call the EIP2612 permit (224), DAI-Style permit (256), Permit2 AllowanceTransfer (192)
/// or Permit2 Signature (96)
/// @param token The address of the token
/// @param data The permit data
/// @param owner The owner of the token (used for Permit2)
/// @param deadline The deadline for the permit (used for Permit2)
/// @param amount The amount to permit (used for Permit2)
function permit(
address token,
bytes calldata data,
address owner,
uint256 deadline,
uint256 amount,
address recipient
)
internal
{
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
// check the permit length
switch data.length
// 0x00 = no permit
case 0x00 {
// do nothing
}
case 0x01 {
// 0x01 is used to signify already existing permit2 allowance
}
// 32(permit2nonce) + 64(signature) = 96 Permit2 Transfer format
case 0x60 {
let x := mload(0x40) // get the free memory pointer
mstore(x, 0x30f28b7a00000000000000000000000000000000000000000000000000000000) // store the selector
mstore(add(x, 0x04), token) // store the srcToken
mstore(add(x, 0x24), amount) // store the amount
calldatacopy(add(x, 0x44), data.offset, 0x20) // copy the nonce
mstore(add(x, 0x64), deadline) // store the deadline
mstore(add(x, 0x84), recipient) // store the recipient address (executor)
mstore(add(x, 0xa4), amount) // store the amount
mstore(add(x, 0xc4), owner) // store the owner
mstore(add(x, 0xe4), 0x100) // store the offset
mstore(add(x, 0x104), 0x40) // store the length
calldatacopy(add(x, 0x124), add(data.offset, 0x20), 0x40) // copy the signature
// Call Permit2 contract and revert on failure
if iszero(call(gas(), PERMIT2_ADDRESS, 0x00, x, 0x164, 0x00, 0x00)) {
mstore(0x00, 0xb78cb0dd00000000000000000000000000000000000000000000000000000000) // store the
// selector
revert(0x00, 0x04) // Revert with PermitFailed error
}
}
// 32(amount) + 32(nonce) + 32(expiration) + 32(sigDeadline) + 64(signature) = 192 Permit2 allowance format
case 0xc0 {
let x := mload(0x40) // get the free memory pointer
mstore(x, 0x2b67b57000000000000000000000000000000000000000000000000000000000) // store the selector
mstore(add(x, 0x04), owner) // store the owner address
mstore(add(x, 0x24), token) // store the token address
calldatacopy(add(x, 0x44), data.offset, 0x60) // copy amount, expiration, nonce
mstore(add(x, 0xa4), address()) // store this contract's address as the spender
calldatacopy(add(x, 0xc4), add(data.offset, 0x60), 0x20) // copy sigDeadline
mstore(add(x, 0xe4), 0x100) // store the offset for signature
mstore(add(x, 0x104), 0x40) // store the length of signature
calldatacopy(add(x, 0x124), add(data.offset, 0x80), 0x40) // copy the signature
// Call Permit2 contract and revert on failure
if iszero(call(gas(), PERMIT2_ADDRESS, 0x00, x, 0x164, 0x00, 0x00)) {
mstore(0x00, 0xb78cb0dd00000000000000000000000000000000000000000000000000000000) // store the
// selector
revert(0x00, 0x04) // Revert with PermitFailed error
}
}
// 32 * 7 = 224 EIP2612 Permit
case 0xe0 {
let x := mload(0x40) // get the free memory pointer
mstore(x, 0xd505accf00000000000000000000000000000000000000000000000000000000) // store the selector
calldatacopy(add(x, 0x04), data.offset, 0xe0) // store the args
pop(call(gas(), token, 0x00, x, 0xe4, 0x00, 0x20)) // call ERC20 permit, skip checking return data
}
// 32 * 8 = 256 DAI-Style Permit
case 0x100 {
let x := mload(0x40) // get the free memory pointer
mstore(x, 0x8fcbaf0c00000000000000000000000000000000000000000000000000000000) // store the selector
calldatacopy(add(x, 0x04), data.offset, 0x100) // store the args
pop(call(gas(), token, 0x00, x, 0x104, 0x00, 0x20)) // call ERC20 permit, skip checking return data
}
// Otherwise revert
default {
mstore(0x00, 0xb78cb0dd00000000000000000000000000000000000000000000000000000000) // store the selector
revert(0x00, 0x04) // Revert with PermitFailed error
}
}
}
/// @dev A fillable version of the permit function from this lib, that doesn't support Permit2 SignatureTransfer
/// @param token The address of the token
/// @param data The permit data
/// @param owner The owner of the token (used for Permit2)
function permit(address token, bytes calldata data, address owner) internal {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
// check the permit length
switch data.length
// 0x00 = no permit
case 0x00 {
// do nothing
}
case 0x01 {
// 0x01 is used to signify already existing permit2 allowance
}
// 32(amount) + 32(nonce) + 32(expiration) + 32(sigDeadline) + 64(signature) = 192 Permit2 allowance format
case 0xc0 {
let x := mload(0x40) // get the free memory pointer
mstore(x, 0x2b67b57000000000000000000000000000000000000000000000000000000000) // store the selector
mstore(add(x, 0x04), owner) // store the owner address
mstore(add(x, 0x24), token) // store the token address
calldatacopy(add(x, 0x44), data.offset, 0x60) // copy amount, expiration, nonce
mstore(add(x, 0xa4), address()) // store this contract's address as the spender
calldatacopy(add(x, 0xc4), add(data.offset, 0x60), 0x20) // copy sigDeadline
mstore(add(x, 0xe4), 0x100) // store the offset for signature
mstore(add(x, 0x104), 0x40) // store the length of signature
calldatacopy(add(x, 0x124), add(data.offset, 0x80), 0x40) // copy the signature
// Call Permit2 contract and revert on failure
if iszero(call(gas(), PERMIT2_ADDRESS, 0x00, x, 0x164, 0x00, 0x00)) {
mstore(0x00, 0xb78cb0dd00000000000000000000000000000000000000000000000000000000) // store the
// selector
revert(0x00, 0x04) // Revert with PermitFailed error
}
}
// 32 * 7 = 224 EIP2612 Permit
case 0xe0 {
let x := mload(0x40) // get the free memory pointer
mstore(x, 0xd505accf00000000000000000000000000000000000000000000000000000000) // store the selector
calldatacopy(add(x, 0x04), data.offset, 0xe0) // store the args
pop(call(gas(), token, 0x00, x, 0xe4, 0x00, 0x20)) // call ERC20 permit, skip checking return data
}
// 32 * 8 = 256 DAI-Style Permit
case 0x100 {
let x := mload(0x40) // get the free memory pointer
mstore(x, 0x8fcbaf0c00000000000000000000000000000000000000000000000000000000) // store the selector
calldatacopy(add(x, 0x04), data.offset, 0x100) // store the args
pop(call(gas(), token, 0x00, x, 0x104, 0x00, 0x20)) // call ERC20 permit, skip checking return data
}
// Otherwise revert
default {
mstore(0x00, 0xb78cb0dd00000000000000000000000000000000000000000000000000000000) // store the selector
revert(0x00, 0x04) // Revert with PermitFailed error
}
}
}
/*//////////////////////////////////////////////////////////////
UTILITIES
//////////////////////////////////////////////////////////////*/
/// @notice Checks if a token is ETH
/// @param token The token address to check
/// @return True if the token represents ETH
function isEth(address token) internal pure returns (bool) {
return token == ETH_ADDRESS;
}
/*//////////////////////////////////////////////////////////////
BALANCE
//////////////////////////////////////////////////////////////*/
/// @dev Returns the balance of address(this), works for both ETH and ERC20 tokens
function getBalance(address token) internal view returns (uint256 balanceOf) {
// solhint-disable-next-line no-inline-assembly
assembly {
switch eq(token, ETH_ADDRESS)
// ETH\
Submitted on: 2025-10-09 11:29:34
Comments
Log in to comment.
No comments yet.