Description:
ERC20 token contract with Factory, Oracle capabilities. Standard implementation for fungible tokens on Ethereum.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/UniExecutor.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
import "./interfaces/IPermit2.sol";
import "./interfaces/IERC20.sol";
import "./interfaces/IUniExecutor.sol";
import "./interfaces/IERC165.sol";
import "./interfaces/IERC1271.sol";
import "./security/PriceValidator.sol";
/// @title UniExecutor
/// @notice Universal executor with Permit2 integration for gasless approvals
/// @dev Supports multicall, conditional execution, Permit2 transfers, ERC-165, and ERC-1271
contract UniExecutor is IUniExecutor, IERC165, IERC1271 {
// ============ Constants ============
address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; // Mainnet Permit2
// ============ State Variables ============
mapping(string => address) public protocols;
mapping(address => bool) public allowedTargets;
address public solver;
mapping(address => bool) public approvedSolvers;
PriceValidator public priceValidator;
bool public paused;
mapping(address => bool) public emergencyOperators;
// ============ Structs ============
// Structs are now defined in IUniExecutor interface
// ============ Events ============
// Main events are now defined in IUniExecutor interface
event ProtocolRegistered(string indexed protocol, address indexed target);
event BatchExecuted(uint256 actionsCount, uint256 gasUsed);
// ============ Errors ============
// Main errors are now defined in IUniExecutor interface
error Permit2InvalidRecipient(address token, address recipient);
error Permit2InsufficientPull(address token, uint256 expected, uint256 received);
// ============ Modifiers ============
modifier onlySolver() {
if (msg.sender != address(this) && !approvedSolvers[msg.sender] && msg.sender != solver) {
revert OnlySolver();
}
_;
}
modifier whenNotPaused() {
if (paused) revert ContractPaused();
_;
}
modifier onlyEmergencyOperator() {
if (!emergencyOperators[msg.sender] && msg.sender != solver) {
revert OnlyEmergencyOperator();
}
_;
}
// Simple non-reentrancy guard for external execution entrypoints
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
bool private _inMulticall;
modifier nonReentrant() {
// Allow internal calls during multicall operations
if (_inMulticall) {
_;
return;
}
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
// ============ Constructor ============
constructor(address _owner) {
solver = _owner;
approvedSolvers[_owner] = true;
emergencyOperators[_owner] = true;
paused = false;
_status = _NOT_ENTERED;
_inMulticall = false;
}
// ============ Admin Functions ============
function setSolver(address newSolver) external onlySolver {
solver = newSolver;
approvedSolvers[newSolver] = true;
}
function addApprovedSolver(address _solver) external onlySolver {
approvedSolvers[_solver] = true;
}
function removeApprovedSolver(address _solver) external onlySolver {
approvedSolvers[_solver] = false;
}
function addEmergencyOperator(address _operator) external onlySolver {
emergencyOperators[_operator] = true;
}
function removeEmergencyOperator(address _operator) external onlySolver {
emergencyOperators[_operator] = false;
}
function setPriceValidator(address _priceValidator) external onlySolver {
priceValidator = PriceValidator(_priceValidator);
}
// ============ Emergency Functions ============
function emergencyPause(string calldata reason) external onlyEmergencyOperator {
paused = true;
emit EmergencyPause(msg.sender, reason);
}
function emergencyUnpause() external onlyEmergencyOperator {
paused = false;
emit EmergencyUnpause(msg.sender);
}
// ============ Protocol Registry ============
function addProtocol(string calldata protocol, address adapter) external onlySolver {
protocols[protocol] = adapter;
allowedTargets[adapter] = true;
emit ProtocolRegistered(protocol, adapter);
}
function registerProtocols(string[] calldata protocolNames, address[] calldata targets) external onlySolver {
require(protocolNames.length == targets.length, "Length mismatch");
for (uint256 i = 0; i < protocolNames.length; i++) {
protocols[protocolNames[i]] = targets[i];
allowedTargets[targets[i]] = true;
emit ProtocolRegistered(protocolNames[i], targets[i]);
}
}
function removeProtocol(string calldata protocol) external onlySolver {
address target = protocols[protocol];
protocols[protocol] = address(0);
if (target != address(0)) {
allowedTargets[target] = false;
}
}
function getProtocol(string calldata protocol) external view returns (address adapter) {
return protocols[protocol];
}
/// @notice Approve a protocol or adapter to spend executor-held tokens
function approveToken(address token, address spender, uint256 amount) external onlySolver {
IERC20(token).approve(spender, amount);
}
// ============ Permit2 Functions ============
/// @notice Execute action with Permit2 for gasless token approvals
/// @param action The action to execute
/// @param permitTransfer Permit2 transfer data including signature
function executeWithPermit2(Action calldata action, Permit2Transfer calldata permitTransfer) external payable {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
if (permitTransfer.owner == address(0)) {
revert Permit2Failed("Invalid owner");
}
// Note: We do NOT check msg.sender == owner to allow relayers to submit transactions
// The Permit2 signature itself validates that the owner authorized this transfer
address recipient = permitTransfer.transferDetails.to;
bool toExecutor = recipient == address(this);
// Allow recipient if it's the executor, an allowed target, or the owner is directly calling
bool recipientAllowed = toExecutor || allowedTargets[recipient] || (msg.sender == permitTransfer.owner);
if (!recipientAllowed) {
revert Permit2InvalidRecipient(permitTransfer.permit.permitted.token, recipient);
}
address balanceAccount = toExecutor ? address(this) : recipient;
uint256 preBalance = IERC20(permitTransfer.permit.permitted.token).balanceOf(balanceAccount);
// First, execute Permit2 transfer to pull tokens from user
_executePermit2Transfer(permitTransfer);
uint256 postBalance = IERC20(permitTransfer.permit.permitted.token).balanceOf(balanceAccount);
uint256 received = postBalance - preBalance;
// Allow 2 wei tolerance for rebasing tokens like stETH that have rounding issues
if (received + 2 < permitTransfer.transferDetails.requestedAmount) {
revert Permit2InsufficientPull(
permitTransfer.permit.permitted.token, permitTransfer.transferDetails.requestedAmount, received
);
}
// Then execute the action
(bool success, bytes memory result) = _handleAction(action, false);
if (!success) {
revert ActionFailed(action.protocol, action.method, result);
}
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
// Execution completed
}
/// @notice Execute batch with Permit2 for multiple token transfers
/// @param actions Array of actions to execute
/// @param permitBatch Batch permit data for multiple tokens
function executeBatchWithPermit2(
Action[] calldata actions,
IPermit2.PermitBatchTransferFrom memory permitBatch,
IPermit2.SignatureTransferDetails[] memory transferDetails,
bytes memory signature,
address owner
) external payable whenNotPaused {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
if (owner == address(0)) revert Permit2Failed("Invalid owner");
// Note: We do NOT check msg.sender == owner to allow relayers to submit transactions
// The Permit2 signature itself validates that the owner authorized this transfer
uint256 permittedLength = permitBatch.permitted.length;
require(permittedLength == transferDetails.length, "Permit2 length mismatch");
uint256[] memory preBalances = new uint256[](permittedLength);
address[] memory balanceAccounts = new address[](permittedLength);
for (uint256 i = 0; i < permittedLength; i++) {
address recipient = transferDetails[i].to;
bool toExecutor = recipient == address(this);
// Allow recipient if it's the executor, an allowed target, or the owner is directly calling
bool recipientAllowed = toExecutor || allowedTargets[recipient] || (msg.sender == owner);
if (!recipientAllowed) {
revert Permit2InvalidRecipient(permitBatch.permitted[i].token, recipient);
}
address balanceAccount = toExecutor ? address(this) : recipient;
balanceAccounts[i] = balanceAccount;
preBalances[i] = IERC20(permitBatch.permitted[i].token).balanceOf(balanceAccount);
}
// Execute batch Permit2 transfer
IPermit2(PERMIT2).permitBatchTransferFrom(permitBatch, transferDetails, owner, signature);
// Log each token transfer
for (uint256 i = 0; i < permittedLength; i++) {
uint256 postBalance = IERC20(permitBatch.permitted[i].token).balanceOf(balanceAccounts[i]);
uint256 received = postBalance - preBalances[i];
// Allow 2 wei tolerance for rebasing tokens like stETH that have rounding issues
if (received + 2 < transferDetails[i].requestedAmount) {
revert Permit2InsufficientPull(
permitBatch.permitted[i].token, transferDetails[i].requestedAmount, received
);
}
emit Permit2Executed(permitBatch.permitted[i].token, transferDetails[i].requestedAmount, owner);
}
// Execute all actions
uint256 gasStart = gasleft();
for (uint256 i = 0; i < actions.length; i++) {
_handleAction(actions[i], false);
}
uint256 gasUsed = gasStart - gasleft();
emit BatchExecuted(actions.length, gasUsed);
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
}
/// @notice Execute batch with Permit2 for multiple token transfers
/// @param actions Array of actions to execute
/// @param permitBatch Batch permit data
/// @param transferDetails Array of transfer details
/// @param owner Owner of the tokens
/// @param signature Permit2 batch signature
/// @param user User address for validation
function executeBatchWithPermit2(
Action[] calldata actions,
IPermit2.PermitBatchTransferFrom calldata permitBatch,
IPermit2.SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes memory signature,
address user
) external payable {
// This is essentially the same as the existing batch permit2 function above
// but with the interface-compatible signature
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
if (owner == address(0)) revert Permit2Failed("Invalid owner");
// Note: We do NOT check msg.sender == owner to allow relayers to submit transactions
// The Permit2 signature itself validates that the owner authorized this transfer
uint256 permittedLength = permitBatch.permitted.length;
require(permittedLength == transferDetails.length, "Permit2 length mismatch");
uint256[] memory preBalances = new uint256[](permittedLength);
address[] memory balanceAccounts = new address[](permittedLength);
for (uint256 i = 0; i < permittedLength; i++) {
address recipient = transferDetails[i].to;
bool toExecutor = recipient == address(this);
// Allow recipient if it's the executor, an allowed target, or the owner is directly calling
bool recipientAllowed = toExecutor || allowedTargets[recipient] || (msg.sender == owner);
if (!recipientAllowed) {
revert Permit2InvalidRecipient(permitBatch.permitted[i].token, recipient);
}
address balanceAccount = toExecutor ? address(this) : recipient;
balanceAccounts[i] = balanceAccount;
preBalances[i] = IERC20(permitBatch.permitted[i].token).balanceOf(balanceAccount);
}
// Execute batch Permit2 transfer
IPermit2(PERMIT2).permitBatchTransferFrom(permitBatch, transferDetails, owner, signature);
// Log each token transfer
for (uint256 i = 0; i < permittedLength; i++) {
uint256 postBalance = IERC20(permitBatch.permitted[i].token).balanceOf(balanceAccounts[i]);
uint256 received = postBalance - preBalances[i];
// Allow 2 wei tolerance for rebasing tokens like stETH that have rounding issues
if (received + 2 < transferDetails[i].requestedAmount) {
revert Permit2InsufficientPull(
permitBatch.permitted[i].token, transferDetails[i].requestedAmount, received
);
}
emit Permit2Executed(permitBatch.permitted[i].token, transferDetails[i].requestedAmount, owner);
}
// Execute all actions
uint256 gasStart = gasleft();
for (uint256 i = 0; i < actions.length; i++) {
_handleAction(actions[i], false);
}
uint256 gasUsed = gasStart - gasleft();
emit BatchExecuted(actions.length, gasUsed);
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
}
// ============ Conditional Execution ============
/// @notice Execute action only if conditions are met
/// @param conditional The conditional action to evaluate and execute
function executeConditional(ConditionalAction calldata conditional) external payable {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
// Check condition
uint256 balance = IERC20(conditional.checkToken).balanceOf(address(this));
if (balance < conditional.minBalance) {
emit ConditionalExecuted(conditional.action.protocol, false, "Insufficient balance");
revert ConditionNotMet(conditional.checkToken, conditional.minBalance, balance);
}
(bool success, bytes memory result) = _handleAction(conditional.action, false);
if (!success) {
revert ActionFailed(conditional.action.protocol, conditional.action.method, result);
}
emit ConditionalExecuted(conditional.action.protocol, true, "Condition met");
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
}
/// @notice Execute multiple conditional actions
/// @param conditionals Array of conditional actions
function executeConditionalBatch(ConditionalAction[] calldata conditionals) external payable {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
for (uint256 i = 0; i < conditionals.length; i++) {
ConditionalAction calldata conditional = conditionals[i];
// Check condition
uint256 balance = IERC20(conditional.checkToken).balanceOf(address(this));
if (balance >= conditional.minBalance) {
_handleAction(conditional.action, false);
emit ConditionalExecuted(conditional.action.protocol, true, "Condition met");
} else {
emit ConditionalExecuted(conditional.action.protocol, false, "Insufficient balance");
if (!conditional.action.skipOnFailure) {
revert ConditionNotMet(conditional.checkToken, conditional.minBalance, balance);
}
}
}
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
}
// ============ Core Execution Functions ============
/// @notice Execute a single action
function executeAction(Action calldata action) external payable {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
(bool success, bytes memory result) = _handleAction(action, false);
if (!success) {
revert ActionFailed(action.protocol, action.method, result);
}
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
// Execution completed
}
/// @notice Execute multiple actions with optional failure handling
function executeBatch(Action[] calldata actions) external payable whenNotPaused {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
uint256 gasStart = gasleft();
for (uint256 i = 0; i < actions.length; i++) {
(bool success, bytes memory result) = _handleAction(actions[i], true);
if (!success && !actions[i].skipOnFailure) {
revert ActionFailed(actions[i].protocol, actions[i].method, result);
}
}
uint256 gasUsed = gasStart - gasleft();
emit BatchExecuted(actions.length, gasUsed);
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
}
/// @notice Enhanced multicall with value splitting
/// @param calls Array of encoded function calls
/// @param values Array of ETH values to send with each call
function multicallWithValue(bytes[] calldata calls, uint256[] calldata values) external payable {
// Custom reentrancy protection for multicall
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
_inMulticall = true;
require(calls.length == values.length, "Length mismatch");
require(msg.value == _sum(values), "Value mismatch");
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = address(this).call{value: values[i]}(calls[i]);
if (!success) {
if (result.length > 0) {
assembly {
revert(add(32, result), mload(result))
}
}
revert("Multicall failed");
}
}
_inMulticall = false;
_status = _NOT_ENTERED;
}
/// @notice Direct call for maximum flexibility
function directCall(address target, bytes calldata callData) external payable returns (bytes memory) {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
require(allowedTargets[target], "Target not allowed");
(bool success, bytes memory result) = target.call{value: msg.value}(callData);
if (!success) {
if (result.length > 0) {
assembly {
revert(add(32, result), mload(result))
}
}
revert("Direct call failed");
}
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
// Execution completed
}
/// @notice Direct call with automatic token forwarding
function directCallWithForwarding(
address target,
bytes calldata callData,
address tokenToForward,
address recipient
) external payable returns (bytes memory) {
// Custom reentrancy protection that works with multicall
if (!_inMulticall) {
require(_status != _ENTERED, "Reentrancy");
_status = _ENTERED;
}
require(allowedTargets[target], "Target not allowed");
address originalCaller = msg.sender;
(bool success, bytes memory result) = target.call{value: msg.value}(callData);
if (!success) {
if (result.length > 0) {
assembly {
revert(add(32, result), mload(result))
}
}
revert("Direct call failed");
}
address forwardRecipient = recipient == address(0) ? originalCaller : recipient;
if (tokenToForward != address(0) && forwardRecipient != address(0)) {
_forwardToken(tokenToForward, forwardRecipient);
}
// Reset reentrancy status if we set it
if (!_inMulticall) {
_status = _NOT_ENTERED;
}
// Execution completed
}
// ============ Internal Functions ============
function _handleAction(Action memory action, bool allowFailure)
internal
returns (bool success, bytes memory result)
{
address target = protocols[action.protocol];
if (target == address(0)) revert ProtocolNotFound(action.protocol);
// Price validation and slippage protection if configured
if (bytes(action.priceAsset).length > 0 && address(priceValidator) != address(0)) {
_validateActionPrice(action);
}
// Record pre-execution balance for slippage check
uint256 preBalance = 0;
if (action.minOutputAmount > 0 && action.token != address(0)) {
preBalance = IERC20(action.token).balanceOf(address(this));
}
(success, result) = target.call{value: action.value}(action.params);
if (!success) {
if (!allowFailure && !action.skipOnFailure) {
revert ActionFailed(action.protocol, action.method, result);
}
} else {
// Post-execution slippage check
if (action.minOutputAmount > 0 && action.token != address(0)) {
uint256 postBalance = IERC20(action.token).balanceOf(address(this));
uint256 actualOutput = postBalance - preBalance;
if (actualOutput < action.minOutputAmount) {
emit SlippageProtected(action.protocol, action.minOutputAmount, actualOutput);
revert SlippageExceeded(action.protocol, action.minOutputAmount, actualOutput);
}
}
// Forward tokens if configured
if (action.forwardTokenBalance && action.token != address(0)) {
address recipient = action.recipient;
if (recipient == address(0)) {
recipient = msg.sender;
}
_forwardToken(action.token, recipient);
}
}
emit ActionExecuted(action.protocol, action.method, success);
return (success, result);
}
function _executePermit2Transfer(Permit2Transfer calldata permitTransfer) internal {
if (permitTransfer.owner == address(0)) {
revert Permit2Failed("Invalid owner");
}
(bool success, bytes memory returndata) = PERMIT2.call(
abi.encodeWithSelector(
IPermit2.permitTransferFrom.selector,
permitTransfer.permit,
permitTransfer.transferDetails,
permitTransfer.owner,
permitTransfer.signature
)
);
if (!success) {
if (returndata.length > 0) {
assembly {
revert(add(32, returndata), mload(returndata))
}
}
revert Permit2Failed("Permit2 call reverted");
}
emit Permit2Executed(
permitTransfer.permit.permitted.token, permitTransfer.transferDetails.requestedAmount, permitTransfer.owner
);
}
function _forwardToken(address token, address recipient) internal {
uint256 balance = IERC20(token).balanceOf(address(this));
if (balance > 0) {
IERC20(token).transfer(recipient, balance);
}
}
function _sum(uint256[] memory values) internal pure returns (uint256 total) {
for (uint256 i = 0; i < values.length; i++) {
total += values[i];
}
}
/// @notice Validate price using RedStone oracle before action execution
/// @param action The action containing price validation parameters
function _validateActionPrice(Action memory action) internal {
if (address(priceValidator) == address(0)) return;
bytes32 assetId = keccak256(abi.encodePacked(action.priceAsset));
uint256 maxSlippage = action.maxSlippageBp > 0 ? action.maxSlippageBp : 500; // Default 5%
try priceValidator.validatePrice(assetId, 0, maxSlippage) returns (uint256 validatedPrice) {
emit PriceValidated(action.priceAsset, validatedPrice, action.minOutputAmount);
} catch Error(string memory reason) {
revert PriceValidationFailed(action.priceAsset, reason);
} catch {
revert PriceValidationFailed(action.priceAsset, "Unknown price validation error");
}
}
// ============ View Functions ============
/// @notice Check if contract is paused
function isPaused() external view returns (bool) {
return paused;
}
/// @notice Get price validator address
function getPriceValidator() external view returns (address) {
return address(priceValidator);
}
/// @notice Check if address is emergency operator
function isEmergencyOperator(address operator) external view returns (bool) {
return emergencyOperators[operator];
}
// ============ Token Recovery ============
/// @notice Recover stuck tokens (emergency function)
function recoverToken(address token, address to, uint256 amount) external onlySolver {
IERC20(token).transfer(to, amount);
}
/// @notice Recover stuck ETH (emergency function)
function recoverETH(address payable to, uint256 amount) external onlySolver {
to.transfer(amount);
}
/// @notice Emergency token recovery when paused
function emergencyRecoverToken(address token, address to, uint256 amount) external onlyEmergencyOperator {
IERC20(token).transfer(to, amount);
}
/// @notice Emergency ETH recovery when paused
function emergencyRecoverETH(address payable to, uint256 amount) external onlyEmergencyOperator {
to.transfer(amount);
}
// ============ ERC-165: Interface Detection ============
/// @notice Query if a contract implements an interface
/// @param interfaceId The interface identifier, as specified in ERC-165
/// @return True if the contract implements interfaceId
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IUniExecutor).interfaceId || interfaceId == type(IERC165).interfaceId
|| interfaceId == type(IERC1271).interfaceId || interfaceId == 0x01ffc9a7; // ERC-165 standard interface ID
}
// ============ ERC-1271: Contract Signature Validation ============
/// @notice Validate signature for contract wallets (ERC-1271)
/// @param hash Hash of the data that was signed
/// @param signature Signature bytes
/// @return magicValue ERC1271_MAGIC_VALUE if valid, ERC1271_INVALID_SIGNATURE otherwise
function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4) {
// Extract signer address from signature
address signer = _recoverSigner(hash, signature);
// Check if signer is an approved solver or emergency operator
if (approvedSolvers[signer] || signer == solver || emergencyOperators[signer]) {
return ERC1271Constants.ERC1271_MAGIC_VALUE;
}
// For Permit2 validation, check if the signer is authorized for the specific operation
if (_isAuthorizedForPermit2(hash, signer)) {
return ERC1271Constants.ERC1271_MAGIC_VALUE;
}
return ERC1271Constants.ERC1271_INVALID_SIGNATURE;
}
/// @notice Recover signer address from hash and signature
/// @param hash The hash that was signed
/// @param signature The signature bytes
/// @return signer The recovered signer address
function _recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address) {
if (signature.length != 65) {
return address(0);
}
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
// Adjust v for Ethereum signature format
if (v < 27) {
v += 27;
}
if (v != 27 && v != 28) {
return address(0);
}
return ecrecover(hash, v, r, s);
}
/// @notice Check if signer is authorized for Permit2 operations
/// @param hash The hash being validated
/// @param signer The recovered signer address
/// @return True if authorized for this specific operation
function _isAuthorizedForPermit2(bytes32 hash, address signer) internal view returns (bool) {
if (signer == address(0) || paused) {
return false;
}
// Check if signer is an authorized EOA
if (_isAuthorizedEOA(signer)) {
return true;
}
// Check if signer is a contract wallet that supports ERC-1271
if (signer.code.length > 0) {
return _isAuthorizedContractWallet(hash, signer);
}
return false;
}
/// @notice Check if an EOA is authorized for Permit2 operations
/// @param signer The signer address to check
/// @return True if authorized EOA
function _isAuthorizedEOA(address signer) internal view returns (bool) {
// Authorized EOAs for Permit2 operations
return signer == solver || approvedSolvers[signer] || emergencyOperators[signer];
}
/// @notice Check if a contract wallet is authorized through ERC-1271
/// @param hash The hash being validated
/// @param contractWallet The contract wallet address
/// @return True if contract wallet validates the signature
function _isAuthorizedContractWallet(bytes32 hash, address contractWallet) internal view returns (bool) {
try IERC165(contractWallet).supportsInterface(type(IERC1271).interfaceId) returns (bool supported) {
if (!supported) {
return false;
}
// For contract wallets, we need the original signature data
// This is a simplified approach - in production, you'd store the signature
// or implement a more sophisticated validation mechanism
bytes memory emptySignature = "";
try IERC1271(contractWallet).isValidSignature(hash, emptySignature) returns (bytes4 magicValue) {
return magicValue == ERC1271Constants.ERC1271_MAGIC_VALUE;
} catch {
return false;
}
} catch {
return false;
}
}
// ============ Testing Helpers ============
/// @notice Public wrapper for testing _isAuthorizedEOA
function isAuthorizedEOA(address signer) external view returns (bool) {
return _isAuthorizedEOA(signer);
}
/// @notice Public wrapper for testing _isAuthorizedContractWallet
function isAuthorizedContractWallet(bytes32 hash, address contractWallet) external view returns (bool) {
return _isAuthorizedContractWallet(hash, contractWallet);
}
/// @notice Public wrapper for testing _isAuthorizedForPermit2
function isAuthorizedForPermit2(bytes32 hash, address signer) external view returns (bool) {
return _isAuthorizedForPermit2(hash, signer);
}
// ============ Receive ETH ============
receive() external payable {}
}
"
},
"src/interfaces/IPermit2.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
/// @title IPermit2
/// @notice Minimal interface for Permit2 functionality
interface IPermit2 {
struct TokenPermissions {
address token;
uint256 amount;
}
struct PermitBatchTransferFrom {
TokenPermissions[] permitted;
uint256 nonce;
uint256 deadline;
}
struct SignatureTransferDetails {
address to;
uint256 requestedAmount;
}
function permitBatchTransferFrom(
PermitBatchTransferFrom memory permit,
SignatureTransferDetails[] memory transferDetails,
address owner,
bytes memory signature
) external;
function permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails memory transferDetails,
address owner,
bytes memory signature
) external;
struct PermitTransferFrom {
TokenPermissions permitted;
uint256 nonce;
uint256 deadline;
}
}
"
},
"src/interfaces/IERC20.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
}
"
},
"src/interfaces/IUniExecutor.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
import "./IPermit2.sol";
/// @title IUniExecutor
/// @notice Interface for the Universal Executor contract
/// @dev Defines all external functions for protocol detection and composition
interface IUniExecutor {
// ============ Structs ============
struct Action {
string protocol;
string method;
bytes params;
uint256 value;
bool skipOnFailure;
address token;
address recipient;
bool forwardTokenBalance;
uint256 minOutputAmount;
string priceAsset;
uint256 maxSlippageBp;
}
struct ConditionalAction {
Action action;
address checkToken;
uint256 minBalance;
}
struct Permit2Transfer {
IPermit2.PermitTransferFrom permit;
IPermit2.SignatureTransferDetails transferDetails;
address owner;
bytes signature;
}
// ============ Events ============
event ActionExecuted(string indexed protocol, string method, bool success);
event Permit2Executed(address token, uint256 amount, address owner);
event ConditionalExecuted(string protocol, bool executed, string reason);
event PriceValidated(string indexed asset, uint256 price, uint256 minOutput);
event SlippageProtected(string protocol, uint256 expectedOutput, uint256 actualOutput);
event EmergencyPause(address operator, string reason);
event EmergencyUnpause(address operator);
// ============ Errors ============
error OnlySolver();
error ProtocolNotFound(string protocol);
error ActionFailed(string protocol, string method, bytes reason);
error Permit2Failed(string reason);
error ConditionNotMet(address token, uint256 required, uint256 actual);
error ContractPaused();
error OnlyEmergencyOperator();
error SlippageExceeded(string protocol, uint256 expected, uint256 actual);
error PriceValidationFailed(string asset, string reason);
// ============ Core Functions ============
/// @notice Execute a single action
/// @param action The action to execute
function executeAction(Action calldata action) external payable;
/// @notice Execute multiple actions in sequence
/// @param actions Array of actions to execute
function executeBatch(Action[] calldata actions) external payable;
/// @notice Execute action with Permit2 for gasless token approvals
/// @param action The action to execute
/// @param permitTransfer Permit2 transfer data including signature
function executeWithPermit2(Action calldata action, Permit2Transfer calldata permitTransfer) external payable;
/// @notice Execute batch with Permit2 for multiple token transfers
/// @param actions Array of actions to execute
/// @param permitBatch Batch permit data
/// @param transferDetails Array of transfer details
/// @param owner Owner of the tokens
/// @param signature Permit2 batch signature
function executeBatchWithPermit2(
Action[] calldata actions,
IPermit2.PermitBatchTransferFrom calldata permitBatch,
IPermit2.SignatureTransferDetails[] calldata transferDetails,
address owner,
bytes memory signature,
address user
) external payable;
/// @notice Execute conditional action based on token balance
/// @param conditional Conditional action with balance requirement
function executeConditional(ConditionalAction calldata conditional) external payable;
/// @notice Execute multiple conditional actions
/// @param conditionals Array of conditional actions
function executeConditionalBatch(ConditionalAction[] calldata conditionals) external payable;
/// @notice Enhanced multicall with value splitting
/// @param calls Array of call data
/// @param values Array of ETH values for each call
function multicallWithValue(bytes[] calldata calls, uint256[] calldata values) external payable;
// ============ Protocol Management ============
/// @notice Add protocol adapter
/// @param protocol Protocol identifier
/// @param adapter Adapter contract address
function addProtocol(string calldata protocol, address adapter) external;
/// @notice Remove protocol adapter
/// @param protocol Protocol identifier to remove
function removeProtocol(string calldata protocol) external;
/// @notice Get protocol adapter address
/// @param protocol Protocol identifier
/// @return adapter The adapter contract address
function getProtocol(string calldata protocol) external view returns (address adapter);
// ============ Access Control ============
/// @notice Set solver address
/// @param newSolver New solver address
function setSolver(address newSolver) external;
/// @notice Add approved solver
/// @param solver Solver address to approve
function addApprovedSolver(address solver) external;
/// @notice Remove approved solver
/// @param solver Solver address to remove
function removeApprovedSolver(address solver) external;
/// @notice Add emergency operator
/// @param operator Emergency operator address
function addEmergencyOperator(address operator) external;
/// @notice Remove emergency operator
/// @param operator Emergency operator address to remove
function removeEmergencyOperator(address operator) external;
// ============ Emergency Controls ============
/// @notice Emergency pause contract
/// @param reason Reason for pausing
function emergencyPause(string calldata reason) external;
/// @notice Emergency unpause contract
function emergencyUnpause() external;
// ============ View Functions ============
/// @notice Get solver address
/// @return The current solver address
function solver() external view returns (address);
/// @notice Check if address is approved solver
/// @param addr Address to check
/// @return True if approved solver
function approvedSolvers(address addr) external view returns (bool);
/// @notice Check if contract is paused
/// @return True if paused
function paused() external view returns (bool);
/// @notice Check if address is emergency operator
/// @param addr Address to check
/// @return True if emergency operator
function emergencyOperators(address addr) external view returns (bool);
}
"
},
"src/interfaces/IERC165.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
/// @title IERC165
/// @notice Interface of the ERC165 standard for interface detection
/// @dev See https://eips.ethereum.org/EIPS/eip-165
interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceId The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceId` and
/// `interfaceId` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
"
},
"src/interfaces/IERC1271.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
/// @title IERC1271
/// @notice Interface for contract signature validation (ERC-1271)
/// @dev See https://eips.ethereum.org/EIPS/eip-1271
interface IERC1271 {
/// @notice Should return whether the signature provided is valid for the provided hash
/// @param hash Hash of the data to be signed
/// @param signature Signature byte array associated with _hash
/// @return magicValue The bytes4 magic value 0x1626ba7e if valid, 0xffffffff otherwise
function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue);
}
/// @notice ERC-1271 constants
library ERC1271Constants {
/// @notice Magic value to be returned upon successful validation
bytes4 internal constant ERC1271_MAGIC_VALUE = 0x1626ba7e;
/// @notice Value to be returned when signature validation fails
bytes4 internal constant ERC1271_INVALID_SIGNATURE = 0xffffffff;
}
"
},
"src/security/PriceValidator.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
import "../interfaces/IRedStoneOracle.sol";
/// @title PriceValidator
/// @notice On-chain price validation using RedStone oracle
/// @dev Provides slippage protection and price staleness checks
contract PriceValidator {
// ============ Constants ============
uint256 public constant MAX_PRICE_AGE = 300; // 5 minutes
uint256 public constant MAX_SLIPPAGE_BP = 1000; // 10% max slippage
uint256 public constant PRICE_DECIMALS = 8; // RedStone uses 8 decimals
// ============ State Variables ============
IRedStoneOracle public oracle;
mapping(bytes32 => uint256) public lastValidPrices;
mapping(bytes32 => uint256) public lastUpdateTimestamps;
// ============ Events ============
event PriceValidated(bytes32 indexed asset, uint256 price, uint256 timestamp);
event SlippageDetected(bytes32 indexed asset, uint256 expected, uint256 actual, uint256 slippageBp);
event PriceStale(bytes32 indexed asset, uint256 age);
// ============ Errors ============
error PriceTooStale(bytes32 asset, uint256 age);
error SlippageExceeded(bytes32 asset, uint256 expectedPrice, uint256 actualPrice);
error InvalidPrice(bytes32 asset);
error OracleError(string reason);
// ============ Constructor ============
constructor(address _oracle) {
oracle = IRedStoneOracle(_oracle);
}
// ============ Price Validation Functions ============
/// @notice Validate price and check for acceptable slippage
/// @param asset The asset identifier (e.g., keccak256("ETH"))
/// @param expectedPrice The expected price with 8 decimals
/// @param maxSlippageBp Maximum acceptable slippage in basis points
/// @return actualPrice The validated price from oracle
function validatePrice(bytes32 asset, uint256 expectedPrice, uint256 maxSlippageBp)
external
returns (uint256 actualPrice)
{
// Get price from RedStone oracle
try oracle.getOracleNumericValueFromTxMsg(asset) returns (uint256 price) {
if (price == 0) revert InvalidPrice(asset);
actualPrice = price;
// Update our records
lastValidPrices[asset] = actualPrice;
lastUpdateTimestamps[asset] = block.timestamp;
emit PriceValidated(asset, actualPrice, block.timestamp);
// Check slippage if expected price provided
if (expectedPrice > 0) {
_validateSlippage(asset, expectedPrice, actualPrice, maxSlippageBp);
}
return actualPrice;
} catch Error(string memory reason) {
revert OracleError(reason);
} catch {
revert OracleError("Unknown oracle error");
}
}
/// @notice Validate multiple prices in batch
/// @param assets Array of asset identifiers
/// @param expectedPrices Array of expected prices (0 to skip slippage check)
/// @param maxSlippageBp Maximum acceptable slippage in basis points
/// @return actualPrices Array of validated prices
function validatePrices(bytes32[] memory assets, uint256[] memory expectedPrices, uint256 maxSlippageBp)
external
returns (uint256[] memory actualPrices)
{
require(assets.length == expectedPrices.length, "Array length mismatch");
try oracle.getOracleNumericValuesFromTxMsg(assets) returns (uint256[] memory prices) {
actualPrices = new uint256[](prices.length);
for (uint256 i = 0; i < assets.length; i++) {
if (prices[i] == 0) revert InvalidPrice(assets[i]);
actualPrices[i] = prices[i];
// Update records
lastValidPrices[assets[i]] = actualPrices[i];
lastUpdateTimestamps[assets[i]] = block.timestamp;
emit PriceValidated(assets[i], actualPrices[i], block.timestamp);
// Check slippage if expected price provided
if (expectedPrices[i] > 0) {
_validateSlippage(assets[i], expectedPrices[i], actualPrices[i], maxSlippageBp);
}
}
return actualPrices;
} catch Error(string memory reason) {
revert OracleError(reason);
} catch {
revert OracleError("Unknown oracle error");
}
}
/// @notice Get last known good price (fallback mechanism)
/// @param asset The asset identifier
/// @param maxAge Maximum acceptable age in seconds
/// @return price The last valid price
/// @return timestamp When price was last updated
function getLastValidPrice(bytes32 asset, uint256 maxAge)
external
view
returns (uint256 price, uint256 timestamp)
{
price = lastValidPrices[asset];
timestamp = lastUpdateTimestamps[asset];
if (price == 0) revert InvalidPrice(asset);
uint256 age = block.timestamp - timestamp;
if (age > maxAge) revert PriceTooStale(asset, age);
return (price, timestamp);
}
/// @notice Calculate acceptable price range for slippage protection
/// @param price The reference price
/// @param slippageBp Slippage tolerance in basis points
/// @return minPrice Minimum acceptable price
/// @return maxPrice Maximum acceptable price
function getPriceRange(uint256 price, uint256 slippageBp)
external
pure
returns (uint256 minPrice, uint256 maxPrice)
{
uint256 slippageAmount = (price * slippageBp) / 10000;
minPrice = price - slippageAmount;
maxPrice = price + slippageAmount;
}
// ============ Internal Functions ============
function _validateSlippage(bytes32 asset, uint256 expectedPrice, uint256 actualPrice, uint256 maxSlippageBp)
internal
{
// Calculate slippage
uint256 priceDiff = expectedPrice > actualPrice ? expectedPrice - actualPrice : actualPrice - expectedPrice;
uint256 slippageBp = (priceDiff * 10000) / expectedPrice;
if (slippageBp > maxSlippageBp) {
emit SlippageDetected(asset, expectedPrice, actualPrice, slippageBp);
revert SlippageExceeded(asset, expectedPrice, actualPrice);
}
}
// ============ View Functions ============
/// @notice Convert string asset name to bytes32 identifier
/// @param assetName Asset name (e.g., "ETH")
/// @return Asset identifier as bytes32
function assetToBytes32(string memory assetName) external pure returns (bytes32) {
return keccak256(abi.encodePacked(assetName));
}
/// @notice Check if price is stale
/// @param asset The asset identifier
/// @return isStale True if price is older than MAX_PRICE_AGE
function isPriceStale(bytes32 asset) external view returns (bool isStale) {
uint256 lastUpdate = lastUpdateTimestamps[asset];
if (lastUpdate == 0) return true;
return (block.timestamp - lastUpdate) > MAX_PRICE_AGE;
}
}
"
},
"src/interfaces/IRedStoneOracle.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.19;
/// @title RedStone Oracle Interface
/// @notice Interface for RedStone pull oracle integration
/// @dev Implements RedStone's pull model for on-chain price feeds
interface IRedStoneOracle {
/// @notice Get price for a given asset
/// @param dataFeedId The asset identifier (e.g., "ETH", "USDC")
/// @return price The price with 8 decimals
function getOracleNumericValueFromTxMsg(bytes32 dataFeedId) external view returns (uint256 price);
/// @notice Get multiple prices in a single call
/// @param dataFeedIds Array of asset identifiers
/// @return prices Array of prices with 8 decimals
function getOracleNumericValuesFromTxMsg(bytes32[] memory dataFeedIds)
external
view
returns (uint256[] memory prices);
/// @notice Validate RedStone signature and timestamp
/// @param dataFeedId The asset identifier
/// @param timestamp The price timestamp
/// @return isValid True if signature and timestamp are valid
function validateRedStoneData(bytes32 dataFeedId, uint256 timestamp) external view returns (bool isValid);
}
/// @title RedStone Core Interface
/// @notice Core RedStone functionality for price validation
interface IRedStoneCore {
/// @notice Extract oracle value from calldata
/// @param dataFeedId The asset identifier
/// @return value The extracted price value
function extractOracleValueFromCalldata(bytes32 dataFeedId) external pure returns (uint256 value);
/// @notice Validate RedStone metadata
/// @return isValid True if metadata is valid
function validateRedStoneMetadata() external view returns (bool isValid);
}
"
}
},
"settings": {
"remappings": [
"forge-std/=lib/forge-std/src/"
],
"optimizer": {
"enabled": true,
"runs": 200
},
"metadata": {
"useLiteralContent": false,
"bytecodeHash": "ipfs",
"appendCBOR": true
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
},
"evmVersion": "prague",
"viaIR": true
}
}}
Submitted on: 2025-10-26 09:47:08
Comments
Log in to comment.
No comments yet.