diff --git a/src/ISettlerActions.sol b/src/ISettlerActions.sol index d6c372c5..e32d9d1e 100644 --- a/src/ISettlerActions.sol +++ b/src/ISettlerActions.sol @@ -44,6 +44,13 @@ interface ISettlerActions { uint256 maxTakerAmount ) external; + function CHAINLINK( + string memory feedName, + int256 priceThreshold, + uint8 expectedDecimals, + uint256 staleThreshold + ) external; + function UNISWAPV4( address recipient, address sellToken, diff --git a/src/Settler.sol b/src/Settler.sol index 0d3acb4c..1fbd4c13 100644 --- a/src/Settler.sol +++ b/src/Settler.sol @@ -95,7 +95,7 @@ abstract contract Settler is Permit2PaymentTakerSubmitted, SettlerBase { if (actions.length != 0) { (uint256 action, bytes calldata data) = actions.decodeCall(0); if (!_dispatchVIP(action, data)) { - if (!_dispatch(0, action, data)) { + if (!_dispatch(action, data)) { revert ActionInvalid(0, bytes4(uint32(action)), data); } } @@ -103,7 +103,7 @@ abstract contract Settler is Permit2PaymentTakerSubmitted, SettlerBase { for (uint256 i = 1; i < actions.length; i = i.unsafeInc()) { (uint256 action, bytes calldata data) = actions.decodeCall(i); - if (!_dispatch(i, action, data)) { + if (!_dispatch(action, data)) { revert ActionInvalid(i, bytes4(uint32(action)), data); } } diff --git a/src/SettlerAbstract.sol b/src/SettlerAbstract.sol index 9ec8c41b..be44233c 100644 --- a/src/SettlerAbstract.sol +++ b/src/SettlerAbstract.sol @@ -5,26 +5,12 @@ import {IERC20} from "@forge-std/interfaces/IERC20.sol"; import {Permit2PaymentAbstract} from "./core/Permit2PaymentAbstract.sol"; abstract contract SettlerAbstract is Permit2PaymentAbstract { - // Permit2 Witness for meta transactions - string internal constant SLIPPAGE_AND_ACTIONS_TYPE = - "SlippageAndActions(address recipient,address buyToken,uint256 minAmountOut,bytes[] actions)"; - bytes32 internal constant SLIPPAGE_AND_ACTIONS_TYPEHASH = - 0x615e8d716cef7295e75dd3f1f10d679914ad6d7759e8e9459f0109ef75241701; - // Permit2 Witness for intents - string internal constant SLIPPAGE_TYPE = "Slippage(address recipient,address buyToken,uint256 minAmountOut)"; - bytes32 internal constant SLIPPAGE_TYPEHASH = 0xdc83993a2ffc65b01b71ed08790b6e39c5c55d76937b62a3b5085b02071f1259; - uint256 internal constant BASIS = 10_000; IERC20 internal constant ETH_ADDRESS = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); - constructor() { - assert(SLIPPAGE_AND_ACTIONS_TYPEHASH == keccak256(bytes(SLIPPAGE_AND_ACTIONS_TYPE))); - assert(SLIPPAGE_TYPEHASH == keccak256(bytes(SLIPPAGE_TYPE))); - } - function _hasMetaTxn() internal pure virtual returns (bool); function _tokenId() internal pure virtual returns (uint256); - function _dispatch(uint256 i, uint256 action, bytes calldata data) internal virtual returns (bool); + function _dispatch(uint256 action, bytes calldata data) internal virtual returns (bool); } diff --git a/src/SettlerBase.sol b/src/SettlerBase.sol index 5237ece0..483a85a0 100644 --- a/src/SettlerBase.sol +++ b/src/SettlerBase.sol @@ -102,7 +102,7 @@ abstract contract SettlerBase is Basic, RfqOrderSettlement, UniswapV3Fork, Unisw } } - function _dispatch(uint256, uint256 action, bytes calldata data) internal virtual override returns (bool) { + function _dispatch(uint256 action, bytes calldata data) internal virtual override returns (bool) { if (action == uint32(ISettlerActions.TRANSFER_FROM.selector)) { (address recipient, ISignatureTransfer.PermitTransferFrom memory permit, bytes memory sig) = abi.decode(data, (address, ISignatureTransfer.PermitTransferFrom, bytes)); diff --git a/src/SettlerIntent.sol b/src/SettlerIntent.sol index 0c29e788..36ba5d68 100644 --- a/src/SettlerIntent.sol +++ b/src/SettlerIntent.sol @@ -1,72 +1,64 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import {SettlerAbstract} from "./SettlerAbstract.sol"; -import {SettlerBase} from "./SettlerBase.sol"; -import {SettlerMetaTxn} from "./SettlerMetaTxn.sol"; +import {SettlerMetaTxnBase} from "./SettlerMetaTxn.sol"; -import {Permit2PaymentAbstract} from "./core/Permit2PaymentAbstract.sol"; -import {Permit2PaymentIntent, Permit2PaymentMetaTxn, Permit2Payment} from "./core/Permit2Payment.sol"; +import {Panic} from "./utils/Panic.sol"; -import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; - -abstract contract SettlerIntent is Permit2PaymentIntent, SettlerMetaTxn { - function _tokenId() internal pure virtual override(SettlerAbstract, SettlerMetaTxn) returns (uint256) { - return 4; +library ArraySliceBecauseSolidityIsDumb { + function slice(bytes[] calldata data, uint256 stop_) internal pure returns (bytes[] calldata rData) { + if (stop_ > data.length) { + Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + } + assembly ("memory-safe") { + rData.offset := data.offset + rData.length := stop_ + } } +} - function _msgSender() - internal - view - virtual - // Solidity inheritance is so stupid - override(Permit2PaymentMetaTxn, SettlerMetaTxn) - returns (address) - { - return super._msgSender(); - } +abstract contract SettlerIntent is SettlerMetaTxnBase { + using ArraySliceBecauseSolidityIsDumb for bytes[]; - function _witnessTypeSuffix() - internal - pure - virtual - // Solidity inheritance is so stupid - override(Permit2PaymentMetaTxn, Permit2PaymentIntent) - returns (string memory) - { - return super._witnessTypeSuffix(); + string internal constant SLIPPAGE_AND_CONDITION_TYPE = + "SlippageAndCondition(address recipient,address buyToken,uint256 minAmountOut,bytes[] condition)"; + bytes32 internal constant SLIPPAGE_AND_CONDITION_TYPEHASH = + 0x24a8d1e812d61f4d1c5a389ec4379906a57587add93708e221ed7965b9ec1c2c; + string internal constant INTENT_WITNESS_TYPE_SUFFIX = + "SlippageAndCondition slippageAndCondition)SlippageAndCondition(address recipient,address buyToken,uint256 minAmountOut,bytes[] condition)TokenPermissions(address token,uint256 amount)"; + + constructor() { + assert(SLIPPAGE_AND_CONDITION_TYPEHASH == keccak256(bytes(SLIPPAGE_AND_CONDITION_TYPE))); + assert( + keccak256(bytes(INTENT_WITNESS_TYPE_SUFFIX)) + == keccak256( + abi.encodePacked( + "SlippageAndCondition slippageAndCondition)", SLIPPAGE_AND_CONDITION_TYPE, TOKEN_PERMISSIONS_TYPE + ) + ) + ); } - function _hashSlippage(AllowedSlippage calldata slippage) internal pure returns (bytes32 result) { - // This function does not check for or clean any dirty bits that might - // exist in `slippage`. We assume that `slippage` will be used elsewhere - // in this context and that if there are dirty bits it will result in a - // revert later. - assembly ("memory-safe") { - let ptr := mload(0x40) - mstore(ptr, SLIPPAGE_TYPEHASH) - calldatacopy(add(ptr, 0x20), slippage, 0x60) - result := keccak256(ptr, 0x80) - } + function _tokenId() internal pure override returns (uint256) { + return 4; } - function executeMetaTxn( + function executeIntent( AllowedSlippage calldata slippage, bytes[] calldata actions, bytes32, /* zid & affiliate */ address msgSender, - bytes calldata sig - ) public virtual override metaTx(msgSender, _hashSlippage(slippage)) returns (bool) { - return _executeMetaTxn(slippage, actions, sig); + bytes calldata sig, + uint256 prefixLen + ) + external + metaTx(msgSender, _hashSlippageAnd(SLIPPAGE_AND_CONDITION_TYPEHASH, actions.slice(prefixLen), slippage)) + returns (bool) + { + return _executeMetaTxn(slippage, actions, sig, prefixLen); } - function _permitToSellAmount(ISignatureTransfer.PermitTransferFrom memory permit) - internal - pure - virtual - override(Permit2PaymentAbstract, Permit2PaymentMetaTxn) - returns (uint256 sellAmount) - { - sellAmount = permit.permitted.amount; + function _witnessTypeSuffix() internal pure override returns (string memory) { + return INTENT_WITNESS_TYPE_SUFFIX; } } diff --git a/src/SettlerMetaTxn.sol b/src/SettlerMetaTxn.sol index 8f740cf7..0dc286e5 100644 --- a/src/SettlerMetaTxn.sol +++ b/src/SettlerMetaTxn.sol @@ -1,160 +1,43 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; - -import {Permit2PaymentMetaTxn} from "./core/Permit2Payment.sol"; - -import {Context, AbstractContext} from "./Context.sol"; -import {CalldataDecoder, SettlerBase} from "./SettlerBase.sol"; -import {UnsafeMath} from "./utils/UnsafeMath.sol"; - -import {ISettlerActions} from "./ISettlerActions.sol"; -import {ConfusedDeputy, ActionInvalid} from "./core/SettlerErrors.sol"; - -abstract contract SettlerMetaTxn is Permit2PaymentMetaTxn, SettlerBase { - using UnsafeMath for uint256; - using CalldataDecoder for bytes[]; - - function _tokenId() internal pure virtual override returns (uint256) { +import {SettlerMetaTxnBase} from "./SettlerMetaTxnBase.sol"; + +abstract contract SettlerMetaTxn is SettlerMetaTxnBase { + string internal constant SLIPPAGE_AND_ACTIONS_TYPE = + "SlippageAndActions(address recipient,address buyToken,uint256 minAmountOut,bytes[] actions)"; + bytes32 internal constant SLIPPAGE_AND_ACTIONS_TYPEHASH = + 0x615e8d716cef7295e75dd3f1f10d679914ad6d7759e8e9459f0109ef75241701; + string internal constant METATX_WITNESS_TYPE_SUFFIX = + "SlippageAndActions slippageAndActions)SlippageAndActions(address recipient,address buyToken,uint256 minAmountOut,bytes[] actions)TokenPermissions(address token,uint256 amount)"; + + constructor() { + assert(SLIPPAGE_AND_ACTIONS_TYPEHASH == keccak256(bytes(SLIPPAGE_AND_ACTIONS_TYPE))); + assert( + keccak256(bytes(METATX_WITNESS_TYPE_SUFFIX)) + == keccak256( + abi.encodePacked( + "SlippageAndActions slippageAndActions)", SLIPPAGE_AND_ACTIONS_TYPE, TOKEN_PERMISSIONS_TYPE + ) + ) + ); + } + + function _tokenId() internal pure override returns (uint256) { return 3; } - function _hasMetaTxn() internal pure override returns (bool) { - return true; - } - - function _msgSender() - internal - view - virtual - // Solidity inheritance is so stupid - override(Permit2PaymentMetaTxn, AbstractContext) - returns (address) - { - return super._msgSender(); - } - - function _hashArrayOfBytes(bytes[] calldata actions) internal pure returns (bytes32 result) { - // This function deliberately does no bounds checking on `actions` for - // gas efficiency. We assume that `actions` will get used elsewhere in - // this context and any OOB or other malformed calldata will result in a - // revert later. - assembly ("memory-safe") { - let ptr := mload(0x40) - let hashesLength := shl(5, actions.length) - for { - let i := actions.offset - let dst := ptr - let end := add(i, hashesLength) - } lt(i, end) { - i := add(i, 0x20) - dst := add(dst, 0x20) - } { - let src := add(actions.offset, calldataload(i)) - let length := calldataload(src) - calldatacopy(dst, add(src, 0x20), length) - mstore(dst, keccak256(dst, length)) - } - result := keccak256(ptr, hashesLength) - } - } - - function _hashActionsAndSlippage(bytes[] calldata actions, AllowedSlippage calldata slippage) - internal - pure - returns (bytes32 result) - { - // This function does not check for or clean any dirty bits that might - // exist in `slippage`. We assume that `slippage` will be used elsewhere - // in this context and that if there are dirty bits it will result in a - // revert later. - bytes32 arrayOfBytesHash = _hashArrayOfBytes(actions); - assembly ("memory-safe") { - let ptr := mload(0x40) - mstore(ptr, SLIPPAGE_AND_ACTIONS_TYPEHASH) - calldatacopy(add(ptr, 0x20), slippage, 0x60) - mstore(add(ptr, 0x80), arrayOfBytesHash) - result := keccak256(ptr, 0xa0) - } - } - - function _dispatchVIP(uint256 action, bytes calldata data, bytes calldata sig) internal virtual returns (bool) { - if (action == uint32(ISettlerActions.METATXN_RFQ_VIP.selector)) { - // An optimized path involving a maker/taker in a single trade - // The RFQ order is signed by both maker and taker, validation is - // performed inside the RfqOrderSettlement so there is no need to - // validate `sig` against `actions` here - ( - address recipient, - ISignatureTransfer.PermitTransferFrom memory makerPermit, - address maker, - bytes memory makerSig, - ISignatureTransfer.PermitTransferFrom memory takerPermit - ) = abi.decode( - data, - (address, ISignatureTransfer.PermitTransferFrom, address, bytes, ISignatureTransfer.PermitTransferFrom) - ); - - fillRfqOrderVIP(recipient, makerPermit, maker, makerSig, takerPermit, sig); - } else if (action == uint32(ISettlerActions.METATXN_TRANSFER_FROM.selector)) { - (address recipient, ISignatureTransfer.PermitTransferFrom memory permit) = - abi.decode(data, (address, ISignatureTransfer.PermitTransferFrom)); - (ISignatureTransfer.SignatureTransferDetails memory transferDetails,) = - _permitToTransferDetails(permit, recipient); - - // We simultaneously transfer-in the taker's tokens and authenticate the - // metatransaction. - _transferFrom(permit, transferDetails, sig); - } else if (action == uint32(ISettlerActions.METATXN_UNISWAPV3_VIP.selector)) { - ( - address recipient, - bytes memory path, - ISignatureTransfer.PermitTransferFrom memory permit, - uint256 amountOutMin - ) = abi.decode(data, (address, bytes, ISignatureTransfer.PermitTransferFrom, uint256)); - - sellToUniswapV3VIP(recipient, path, permit, sig, amountOutMin); - } else { - return false; - } - return true; - } - - function _executeMetaTxn(AllowedSlippage calldata slippage, bytes[] calldata actions, bytes calldata sig) - internal - returns (bool) - { - require(actions.length != 0); - { - (uint256 action, bytes calldata data) = actions.decodeCall(0); - - // By forcing the first action to be one of the witness-aware - // actions, we ensure that the entire sequence of actions is - // authorized. `msgSender` is the signer of the metatransaction. - if (!_dispatchVIP(action, data, sig)) { - revert ActionInvalid(0, bytes4(uint32(action)), data); - } - } - - for (uint256 i = 1; i < actions.length; i = i.unsafeInc()) { - (uint256 action, bytes calldata data) = actions.decodeCall(i); - if (!_dispatch(i, action, data)) { - revert ActionInvalid(i, bytes4(uint32(action)), data); - } - } - - _checkSlippageAndTransfer(slippage); - return true; - } - function executeMetaTxn( AllowedSlippage calldata slippage, bytes[] calldata actions, bytes32, /* zid & affiliate */ address msgSender, bytes calldata sig - ) public virtual metaTx(msgSender, _hashActionsAndSlippage(actions, slippage)) returns (bool) { - return _executeMetaTxn(slippage, actions, sig); + ) external metaTx(msgSender, _hashSlippageAnd(SLIPPAGE_AND_ACTIONS_TYPEHASH, actions, slippage)) returns (bool) { + return _executeMetaTxn(slippage, actions, sig, 0); + } + + function _witnessTypeSuffix() internal pure override returns (string memory) { + return METATX_WITNESS_TYPE_SUFFIX; } } diff --git a/src/SettlerMetaTxnBase.sol b/src/SettlerMetaTxnBase.sol new file mode 100644 index 00000000..1f5ca4b8 --- /dev/null +++ b/src/SettlerMetaTxnBase.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; + +import {Permit2PaymentMetaTxn} from "./core/Permit2Payment.sol"; + +import {Context, AbstractContext} from "./Context.sol"; +import {CalldataDecoder, SettlerBase} from "./SettlerBase.sol"; +import {UnsafeMath} from "./utils/UnsafeMath.sol"; + +import {ISettlerActions} from "./ISettlerActions.sol"; +import {ConfusedDeputy, ActionInvalid} from "./core/SettlerErrors.sol"; + +abstract contract SettlerMetaTxnBase is Permit2PaymentMetaTxn, SettlerBase { + using UnsafeMath for uint256; + using CalldataDecoder for bytes[]; + + function _hasMetaTxn() internal pure override returns (bool) { + return true; + } + + function _hashArrayOfBytes(bytes[] calldata actions) internal pure returns (bytes32 result) { + // This function deliberately does no bounds checking on `actions` for + // gas efficiency. We assume that `actions` will get used elsewhere in + // this context and any OOB or other malformed calldata will result in a + // revert later. + assembly ("memory-safe") { + let ptr := mload(0x40) + let hashesLength := shl(0x05, actions.length) + for { + let i := actions.offset + let dst := ptr + let end := add(i, hashesLength) + } lt(i, end) { + i := add(0x20, i) + dst := add(0x20, dst) + } { + let src := add(actions.offset, calldataload(i)) + let length := calldataload(src) + calldatacopy(dst, add(0x20, src), length) + mstore(dst, keccak256(dst, length)) + } + result := keccak256(ptr, hashesLength) + } + } + + function _hashSlippageAnd(bytes32 typehash, bytes[] calldata actions, AllowedSlippage calldata slippage) + internal + pure + returns (bytes32 result) + { + // This function does not check for or clean any dirty bits that might + // exist in `slippage`. We assume that `slippage` will be used elsewhere + // in this context and that if there are dirty bits it will result in a + // revert later. + bytes32 arrayOfBytesHash = _hashArrayOfBytes(actions); + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(ptr, typehash) + calldatacopy(add(0x20, ptr), slippage, 0x60) + mstore(add(0x80, ptr), arrayOfBytesHash) + result := keccak256(ptr, 0xa0) + } + } + + function _dispatchVIP(uint256 action, bytes calldata data, bytes calldata sig) internal virtual returns (bool) { + if (action == uint32(ISettlerActions.METATXN_RFQ_VIP.selector)) { + // An optimized path involving a maker/taker in a single trade + // The RFQ order is signed by both maker and taker, validation is + // performed inside the RfqOrderSettlement so there is no need to + // validate `sig` against `actions` here + ( + address recipient, + ISignatureTransfer.PermitTransferFrom memory makerPermit, + address maker, + bytes memory makerSig, + ISignatureTransfer.PermitTransferFrom memory takerPermit + ) = abi.decode( + data, + (address, ISignatureTransfer.PermitTransferFrom, address, bytes, ISignatureTransfer.PermitTransferFrom) + ); + + fillRfqOrderVIP(recipient, makerPermit, maker, makerSig, takerPermit, sig); + } else if (action == uint32(ISettlerActions.METATXN_TRANSFER_FROM.selector)) { + (address recipient, ISignatureTransfer.PermitTransferFrom memory permit) = + abi.decode(data, (address, ISignatureTransfer.PermitTransferFrom)); + (ISignatureTransfer.SignatureTransferDetails memory transferDetails,) = + _permitToTransferDetails(permit, recipient); + + // We simultaneously transfer-in the taker's tokens and authenticate the + // metatransaction. + _transferFrom(permit, transferDetails, sig); + } else if (action == uint32(ISettlerActions.METATXN_UNISWAPV3_VIP.selector)) { + ( + address recipient, + bytes memory path, + ISignatureTransfer.PermitTransferFrom memory permit, + uint256 amountOutMin + ) = abi.decode(data, (address, bytes, ISignatureTransfer.PermitTransferFrom, uint256)); + + sellToUniswapV3VIP(recipient, path, permit, sig, amountOutMin); + } else { + return false; + } + return true; + } + + function _executeMetaTxn( + AllowedSlippage calldata slippage, + bytes[] calldata actions, + bytes calldata sig, + uint256 fundingActionIndex + ) internal returns (bool) { + require(actions.length > fundingActionIndex); + + uint256 i; + for (; i < fundingActionIndex; i = i.unsafeInc()) { + (uint256 action, bytes calldata data) = actions.decodeCall(i); + if (!_dispatch(action, data)) { + revert ActionInvalid(i, bytes4(uint32(action)), data); + } + } + + { + (uint256 action, bytes calldata data) = actions.decodeCall(i); + if (!_dispatchVIP(action, data, sig)) { + revert ActionInvalid(i, bytes4(uint32(action)), data); + } + } + + i = i.unsafeInc(); + for (; i < actions.length; i = i.unsafeInc()) { + (uint256 action, bytes calldata data) = actions.decodeCall(i); + if (!_dispatch(action, data)) { + revert ActionInvalid(i, bytes4(uint32(action)), data); + } + } + + _checkSlippageAndTransfer(slippage); + return true; + } +} diff --git a/src/chains/Mainnet/Common.sol b/src/chains/Mainnet/Common.sol index 9ec8f176..4ee2c682 100644 --- a/src/chains/Mainnet/Common.sol +++ b/src/chains/Mainnet/Common.sol @@ -56,14 +56,14 @@ abstract contract MainnetMixin is assert(block.chainid == 1 || block.chainid == 31337); } - function _dispatch(uint256 i, uint256 action, bytes calldata data) + function _dispatch(uint256 action, bytes calldata data) internal virtual override(SettlerAbstract, SettlerBase) DANGEROUS_freeMemory returns (bool) { - if (super._dispatch(i, action, data)) { + if (super._dispatch(action, data)) { return true; } else if (action == uint32(ISettlerActions.UNISWAPV4.selector)) { ( diff --git a/src/chains/Mainnet/Intent.sol b/src/chains/Mainnet/Intent.sol index 5de25267..8a351af1 100644 --- a/src/chains/Mainnet/Intent.sol +++ b/src/chains/Mainnet/Intent.sol @@ -1,76 +1,44 @@ // SPDX-License-Identifier: MIT pragma solidity =0.8.25; -import {MainnetSettlerMetaTxn} from "./MetaTxn.sol"; import {SettlerIntent} from "../../SettlerIntent.sol"; +import {MainnetSettlerMetaTxnBase} from "./MetaTxn.sol"; +import {Chainlink} from "../../core/Chainlink.sol"; -import {IERC20} from "@forge-std/interfaces/IERC20.sol"; -import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; import {ISettlerActions} from "../../ISettlerActions.sol"; // Solidity inheritance is stupid import {SettlerAbstract} from "../../SettlerAbstract.sol"; import {SettlerBase} from "../../SettlerBase.sol"; -import {SettlerMetaTxn} from "../../SettlerMetaTxn.sol"; -import {SettlerIntent} from "../../SettlerIntent.sol"; -import {AbstractContext} from "../../Context.sol"; -import {Permit2PaymentAbstract} from "../../core/Permit2PaymentAbstract.sol"; -import {Permit2PaymentMetaTxn} from "../../core/Permit2Payment.sol"; +import {SettlerMetaTxnBase} from "../../SettlerMetaTxn.sol"; /// @custom:security-contact security@0x.org -contract MainnetSettlerIntent is SettlerIntent, MainnetSettlerMetaTxn { - constructor(bytes20 gitCommit) MainnetSettlerMetaTxn(gitCommit) {} +contract MainnetSettlerIntent is SettlerIntent, MainnetSettlerMetaTxnBase, Chainlink { + constructor(bytes20 gitCommit) SettlerBase(gitCommit) {} - // Solidity inheritance is stupid - function executeMetaTxn( - AllowedSlippage calldata slippage, - bytes[] calldata actions, - bytes32, /* zid & affiliate */ - address msgSender, - bytes calldata sig - ) public override(SettlerIntent, SettlerMetaTxn) returns (bool) { - return super.executeMetaTxn(slippage, actions, bytes32(0), msgSender, sig); - } - - function _dispatch(uint256 i, uint256 action, bytes calldata data) + function _dispatch(uint256 action, bytes calldata data) internal - override(MainnetSettlerMetaTxn, SettlerBase, SettlerAbstract) + override(MainnetSettlerMetaTxnBase, SettlerBase, SettlerAbstract) returns (bool) { - return super._dispatch(i, action, data); - } - - function _msgSender() internal view override(SettlerIntent, MainnetSettlerMetaTxn) returns (address) { - return super._msgSender(); - } - - function _witnessTypeSuffix() - internal - pure - override(SettlerIntent, Permit2PaymentMetaTxn) - returns (string memory) - { - return super._witnessTypeSuffix(); - } - - function _tokenId() internal pure override(SettlerIntent, SettlerMetaTxn, SettlerAbstract) returns (uint256) { - return super._tokenId(); + if (super._dispatch(action, data)) { + return true; + } else if (action == uint32(ISettlerActions.CHAINLINK.selector)) { + (string memory feedName, int256 priceThreshold, uint8 expectedDecimals, uint256 staleThreshold) = abi.decode(data, (string, int256, uint8, uint256)); + + consultChainlink(feedName, priceThreshold, expectedDecimals, staleThreshold); + } else { + return false; + } + return true; } + // Solidity inheritance is stupid function _dispatchVIP(uint256 action, bytes calldata data, bytes calldata sig) internal - override(MainnetSettlerMetaTxn, SettlerMetaTxn) + override(MainnetSettlerMetaTxnBase, SettlerMetaTxnBase) returns (bool) { return super._dispatchVIP(action, data, sig); } - - function _permitToSellAmount(ISignatureTransfer.PermitTransferFrom memory permit) - internal - pure - override(SettlerIntent, Permit2PaymentAbstract, Permit2PaymentMetaTxn) - returns (uint256) - { - return super._permitToSellAmount(permit); - } } diff --git a/src/chains/Mainnet/MetaTxn.sol b/src/chains/Mainnet/MetaTxn.sol index c926fb8b..7f7b53e5 100644 --- a/src/chains/Mainnet/MetaTxn.sol +++ b/src/chains/Mainnet/MetaTxn.sol @@ -2,21 +2,17 @@ pragma solidity =0.8.25; import {MainnetMixin} from "./Common.sol"; +import {SettlerMetaTxnBase} from "../../SettlerMetaTxnBase.sol"; import {SettlerMetaTxn} from "../../SettlerMetaTxn.sol"; -import {IERC20} from "@forge-std/interfaces/IERC20.sol"; import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; import {ISettlerActions} from "../../ISettlerActions.sol"; // Solidity inheritance is stupid import {SettlerAbstract} from "../../SettlerAbstract.sol"; import {SettlerBase} from "../../SettlerBase.sol"; -import {AbstractContext} from "../../Context.sol"; - -/// @custom:security-contact security@0x.org -contract MainnetSettlerMetaTxn is SettlerMetaTxn, MainnetMixin { - constructor(bytes20 gitCommit) SettlerBase(gitCommit) {} +abstract contract MainnetSettlerMetaTxnBase is SettlerMetaTxnBase, MainnetMixin { function _dispatchVIP(uint256 action, bytes calldata data, bytes calldata sig) internal virtual @@ -66,16 +62,34 @@ contract MainnetSettlerMetaTxn is SettlerMetaTxn, MainnetMixin { } // Solidity inheritance is stupid - function _dispatch(uint256 i, uint256 action, bytes calldata data) + function _dispatch(uint256 action, bytes calldata data) internal virtual override(SettlerAbstract, SettlerBase, MainnetMixin) returns (bool) { - return super._dispatch(i, action, data); + return super._dispatch(action, data); } +} - function _msgSender() internal view virtual override(SettlerMetaTxn, AbstractContext) returns (address) { - return super._msgSender(); +/// @custom:security-contact security@0x.org +contract MainnetSettlerMetaTxn is SettlerMetaTxn, MainnetSettlerMetaTxnBase { + constructor(bytes20 gitCommit) SettlerBase(gitCommit) {} + + // Solidity inheritance is stupid + function _dispatch(uint256 action, bytes calldata data) + internal + override(MainnetSettlerMetaTxnBase, SettlerBase, SettlerAbstract) + returns (bool) + { + return super._dispatch(action, data); + } + + function _dispatchVIP(uint256 action, bytes calldata data, bytes calldata sig) + internal + override(MainnetSettlerMetaTxnBase, SettlerMetaTxnBase) + returns (bool) + { + return super._dispatchVIP(action, data, sig); } } diff --git a/src/chains/Mainnet/TakerSubmitted.sol b/src/chains/Mainnet/TakerSubmitted.sol index c708cfbb..6b4d5f90 100644 --- a/src/chains/Mainnet/TakerSubmitted.sol +++ b/src/chains/Mainnet/TakerSubmitted.sol @@ -64,24 +64,11 @@ contract MainnetSettler is Settler, MainnetMixin { } // Solidity inheritance is stupid - function _isRestrictedTarget(address target) - internal - pure - override(Settler, Permit2PaymentAbstract) - returns (bool) - { - return super._isRestrictedTarget(target); - } - - function _dispatch(uint256 i, uint256 action, bytes calldata data) + function _dispatch(uint256 action, bytes calldata data) internal override(SettlerAbstract, SettlerBase, MainnetMixin) returns (bool) { - return super._dispatch(i, action, data); - } - - function _msgSender() internal view override(Settler, AbstractContext) returns (address) { - return super._msgSender(); + return super._dispatch(action, data); } } diff --git a/src/chains/Mode/MetaTxn.sol b/src/chains/Mode/MetaTxn.sol index 3e6f3068..03bdf7f2 100644 --- a/src/chains/Mode/MetaTxn.sol +++ b/src/chains/Mode/MetaTxn.sol @@ -21,8 +21,8 @@ contract ModeSettlerMetaTxn is SettlerMetaTxn, ModeMixin { function _dispatchVIP(uint256 action, bytes calldata data, bytes calldata sig) internal - override virtual + override DANGEROUS_freeMemory returns (bool) { diff --git a/src/core/Chainlink.sol b/src/core/Chainlink.sol new file mode 100644 index 00000000..799c9cd2 --- /dev/null +++ b/src/core/Chainlink.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {ENS} from "../utils/ENS.sol"; +import {Panic} from "../utils/Panic.sol"; +import {UnsafeMath} from "../utils/UnsafeMath.sol"; + +import {StalePriceData, PriceTooHigh, PriceTooLow} from "./SettlerErrors.sol"; + +import {SettlerAbstract} from "../SettlerAbstract.sol"; + +interface IAggregatorV3 { + function decimals() external view returns (uint8); + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 /* startedAt */, uint256 updatedAt); // uint80 answeredInRound +} + +abstract contract Chainlink is SettlerAbstract { + using ENS for string; + using ENS for bytes32; + using UnsafeMath for int256; + using UnsafeMath for uint8; + + // keccak256(bytes.concat( + // keccak256(bytes.concat( + // bytes32(0), + // keccak256("eth") + // )), + // keccak256("data") + // )) + bytes32 private constant _DATA_DOT_ETH_NODE = 0x4a9dd6923a809a49d009b308182940df46ac3a45ee16c1133f90db66596dae1f; + // keccak256("aggregator") + bytes32 private constant _AGGREGATOR_NODE = 0xe124d7cc79a19705865fa21b784ba187cd393559e960c0c071132cb60354d1a3; + + function consultChainlink(string memory feedName, int256 priceThreshold, uint8 expectedDecimals, uint256 staleThreshold) internal view { + // namehash of `string.concat("aggregator.", feedName, ".data.eth")` + bytes32 node; + assembly ("memory-safe") { + mstore(0x00, _DATA_DOT_ETH_NODE) + mstore(0x20, keccak256(add(0x20, feedName), mload(feedName))) + mstore(0x00, keccak256(0x00, 0x40)) + mstore(0x20, _AGGREGATOR_NODE) + node := keccak256(0x00, 0x40) + } + + // resolve the ENS node + IAggregatorV3 aggregator = IAggregatorV3(node.toAddr()); + + // query the oracle + (uint80 roundId, int256 answer,, uint256 updatedAt) = aggregator.latestRoundData(); + if (roundId == 0 || block.timestamp - updatedAt > staleThreshold) { + revert StalePriceData(roundId, answer, updatedAt); + } + if (answer <= 0) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + + // adjust for decimals + uint8 decimals = aggregator.decimals(); + if (decimals > expectedDecimals) { + uint256 shift = decimals.unsafeSub(expectedDecimals); + if (shift == 255) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + priceThreshold *= int256(1 << shift); + } else if (decimals < expectedDecimals) { + uint256 shift = expectedDecimals.unsafeSub(decimals); + if (shift == 255) { + Panic.panic(Panic.ARITHMETIC_OVERFLOW); + } + answer *= int256(1 << shift); + } + + // check the price + if (priceThreshold < 0) { + if (priceThreshold.unsafeNeg() > answer) { + revert PriceTooHigh(answer, priceThreshold.unsafeNeg()); + } + } else { + if (priceThreshold < answer) { + revert PriceTooLow(answer, priceThreshold); + } + } + } +} diff --git a/src/core/Permit2Payment.sol b/src/core/Permit2Payment.sol index e0ce92d2..a7e1d90f 100644 --- a/src/core/Permit2Payment.sol +++ b/src/core/Permit2Payment.sol @@ -403,13 +403,7 @@ abstract contract Permit2PaymentMetaTxn is Context, Permit2Payment { return Permit2PaymentBase._msgSender(); } - function _witnessTypeSuffix() internal pure virtual returns (string memory) { - return string( - abi.encodePacked( - "SlippageAndActions slippageAndActions)", SLIPPAGE_AND_ACTIONS_TYPE, TOKEN_PERMISSIONS_TYPE - ) - ); - } + function _witnessTypeSuffix() internal pure virtual returns (string memory); function _transferFrom( ISignatureTransfer.PermitTransferFrom memory permit, @@ -444,13 +438,7 @@ abstract contract Permit2PaymentMetaTxn is Context, Permit2Payment { _; TransientStorage.clearPayer(msgSender); // It should not be possible for this check to revert because the very first thing that a - // metatransaction does is spend the witness. + // metatransaction does (after checking the precondition, if present) is spend the witness. TransientStorage.checkSpentWitness(); } } - -abstract contract Permit2PaymentIntent is Permit2PaymentMetaTxn { - function _witnessTypeSuffix() internal pure virtual override returns (string memory) { - return string(abi.encodePacked("Slippage slippage)", SLIPPAGE_TYPE, TOKEN_PERMISSIONS_TYPE)); - } -} diff --git a/src/core/SettlerErrors.sol b/src/core/SettlerErrors.sol index 70901704..b8d07a76 100644 --- a/src/core/SettlerErrors.sol +++ b/src/core/SettlerErrors.sol @@ -65,3 +65,7 @@ error ZeroBuyAmount(IERC20 buyToken); error BoughtSellToken(IERC20 sellToken); error TokenHashCollision(IERC20 token0, IERC20 token1); error ZeroToken(); + +error StalePriceData(uint80 roundId, int256 answer, uint256 updatedAt); +error PriceTooHigh(int256 actual, int256 expected); +error PriceTooLow(int256 actual, int256 expected); diff --git a/src/utils/CallWithGas.sol b/src/utils/CallWithGas.sol new file mode 100644 index 00000000..0191efe4 --- /dev/null +++ b/src/utils/CallWithGas.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +library CallWithGas { + error ReturnDataTooLong(uint256 returnDataLength); + + /** + * @notice `staticcall` another contract forwarding a precomputed amount of gas. + * @dev contains protections against EIP-150-induced insufficient gas griefing + * @dev reverts iff the target is not a contract or we encounter an out-of-gas + * @return success true iff the call succeded and returned no more than + * `maxReturnBytes` of return data + * @return returnData the return data or revert reason of the call + * @param target the contract (reverts if non-contract) on which to make the + * `staticcall` + * @param data the calldata to pass + * @param callGas the gas to pass for the call. If the call requires more than the + * specified amount of gas and the caller didn't provide at least + * `callGas`, triggers an out-of-gas in the caller. + * @param maxReturnBytes Only this many bytes of return data are read back from the + * call. This prevents griefing the caller. If more bytes are + * returned or the revert reason is longer, success will be + * false and returnData will be + * `abi.encodeWithSignature("ReturnDataTooLong(uint256)", returndatasize())` + */ + function functionStaticCallWithGas(address target, bytes memory data, uint256 callGas, uint256 maxReturnBytes) + internal + view + returns (bool success, bytes memory returnData) + { + assembly ("memory-safe") { + returnData := mload(0x40) + success := staticcall(callGas, target, add(data, 0x20), mload(data), add(returnData, 0x20), maxReturnBytes) + + // `verbatim` can't work in inline assembly. Assignment of a value to a + // variable costs gas (although how much is unpredictable because it depends + // on the Yul/IR optimizer), as does the `GAS` opcode itself. Therefore, the + // `gas()` below returns less than the actual amount of gas available for + // computation at the end of the call. That makes this check slightly too + // conservative. However, we do not correct for this because the correction + // would become outdated (possibly too permissive) if the opcodes are + // repriced. + + // https://eips.ethereum.org/EIPS/eip-150 + // https://ronan.eth.limo/blog/ethereum-gas-dangers/ + if iszero(or(success, or(returndatasize(), lt(div(callGas, 63), gas())))) { + // The call failed due to not enough gas left. We deliberately consume + // all remaining gas with `invalid` (instead of `revert`) to make this + // failure distinguishable to our caller. + invalid() + } + + switch gt(returndatasize(), maxReturnBytes) + case 0 { + switch returndatasize() + case 0 { + returnData := 0x60 + success := and(success, iszero(iszero(extcodesize(target)))) + } + default { + mstore(returnData, returndatasize()) + mstore(0x40, add(returnData, add(0x20, returndatasize()))) + } + } + default { + // returnData = abi.encodeWithSignature("ReturnDataTooLong(uint256)", returndatasize()) + success := 0 + mstore(add(0x04, returnData), 0x0064b2c7) // selector for `ReturnDataTooLong(uint256)` + mstore(returnData, 0x24) + mstore(add(0x24, returnData), returndatasize()) + mstore(0x40, add(returnData, 0x44)) + } + } + } + + /// See `functionCallWithGasAndValue` + function functionCallWithGas(address target, bytes memory data, uint256 callGas, uint256 maxReturnBytes) + internal + returns (bool success, bytes memory returnData) + { + return functionCallWithGasAndValue(payable(target), data, callGas, 0, maxReturnBytes); + } + + /** + * @notice `call` another contract forwarding a precomputed amount of gas. + * @notice Unlike `functionStaticCallWithGas`, a failure is not signaled if there is + * too much return data. Instead, it is simply truncated. + * @dev contains protections against EIP-150-induced insufficient gas griefing + * @dev reverts iff caller doesn't have enough native asset balance, the target is + * not a contract, or due to out-of-gas + * @return success true iff the call succeded + * @return returnData the return data or revert reason of the call + * @param target the contract (reverts if non-contract) on which to make the `call` + * @param data the calldata to pass + * @param callGas the gas to pass for the call. If the call requires more than the + * specified amount of gas and the caller didn't provide at least + * `callGas`, triggers an out-of-gas in the caller. + * @param value the amount of the native asset in wei to pass to the callee with the + * call + * @param maxReturnBytes Only this many bytes of return data/revert reason are read + * back from the call. This prevents griefing the caller. If + * more bytes are returned or the revert reason is longer, + * returnData will be truncated + */ + function functionCallWithGasAndValue( + address payable target, + bytes memory data, + uint256 callGas, + uint256 value, + uint256 maxReturnBytes + ) internal returns (bool success, bytes memory returnData) { + if (value > 0 && (address(this).balance < value || target.code.length == 0)) { + return (success, returnData); + } + + assembly ("memory-safe") { + returnData := mload(0x40) + success := call(callGas, target, value, add(data, 0x20), mload(data), add(returnData, 0x20), maxReturnBytes) + + // `verbatim` can't work in inline assembly. Assignment of a value to a + // variable costs gas (although how much is unpredictable because it depends + // on the Yul/IR optimizer), as does the `GAS` opcode itself. Therefore, the + // `gas()` below returns less than the actual amount of gas available for + // computation at the end of the call. That makes this check slightly too + // conservative. However, we do not correct for this because the correction + // would become outdated (possibly too permissive) if the opcodes are + // repriced. + + // https://eips.ethereum.org/EIPS/eip-150 + // https://ronan.eth.limo/blog/ethereum-gas-dangers/ + if iszero(or(success, or(returndatasize(), lt(div(callGas, 63), gas())))) { + // The call failed due to not enough gas left. We deliberately consume + // all remaining gas with `invalid` (instead of `revert`) to make this + // failure distinguishable to our caller. + invalid() + } + + switch gt(returndatasize(), maxReturnBytes) + case 0 { + switch returndatasize() + case 0 { + returnData := 0x60 + if iszero(value) { success := and(success, iszero(iszero(extcodesize(target)))) } + } + default { + mstore(returnData, returndatasize()) + mstore(0x40, add(returnData, add(0x20, returndatasize()))) + } + } + default { + mstore(returnData, maxReturnBytes) + mstore(0x40, add(add(0x20, maxReturnBytes), returnData)) + } + } + } +} diff --git a/src/utils/ENS.sol b/src/utils/ENS.sol new file mode 100644 index 00000000..d8513f94 --- /dev/null +++ b/src/utils/ENS.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {ERC165Checker} from "./ERC165Checker.sol"; +import {IERC165} from "@forge-std/interfaces/IERC165.sol"; + +interface Resolver is IERC165 { + function addr(bytes32 node) external view returns (address); + + function name(bytes32 node) external view returns (string memory); +} + +interface Registry { + function resolver(bytes32 node) external view returns (Resolver); +} + +library ENS { + using ERC165Checker for address; + + Registry private constant _REGISTRY = Registry(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e); + bytes32 private constant _ETH_NODE = 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae; // namehash of "eth" + bytes32 private constant _REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; // namehash of "addr.reverse" + + error ENSRoot(); + error NoResolver(bytes32 node); + error NoRecord(bytes32 node, address resolver); + error InvalidEthName(string name); + error NoReverseResolver(address addr); + error NoReverseRecord(address addr, address resolver); + error ReverseForwardMismatch(address addr, bytes32 node, address forward); + + function toAddr(bytes32 node) internal view returns (address result) { + if (node == bytes32(0)) { + revert ENSRoot(); + } + Resolver resolver = _REGISTRY.resolver(node); + if (!address(resolver).supportsInterface(Resolver.addr.selector)) { + revert NoResolver(node); + } + result = resolver.addr(node); + if (result == address(0)) { + revert NoRecord(node, address(resolver)); + } + } + + // returns namehash of hex(addr).addr.reverse + // (e.g. "112234455c3a32fd11230c42e7bccd4a84e02010.addr.reverse") + function reverseNode(address addr) internal pure returns (bytes32 node) { + assembly ("memory-safe") { + for { + let i := 0x28 + let lookup := 0x3031323334353637383961626364656600000000000000000000000000000000 + } i {} { + i := sub(i, 1) + mstore8(i, byte(and(addr, 0x0f), lookup)) + addr := shr(4, addr) + i := sub(i, 1) + mstore8(i, byte(and(addr, 0x0f), lookup)) + addr := shr(4, addr) + } + mstore(0x20, keccak256(0x00, 0x28)) + mstore(0x00, _REVERSE_NODE) + node := keccak256(0x00, 0x40) + } + } + + // check that `name` is part of the *.eth hierarchy and is non-confusable; + // namehash it + function _nameHash(string memory name) private pure returns (bytes32 node) { + // This is written in Yul because memory string slicing doesn't work in + // Solidity + assembly ("memory-safe") { + let i := add(name, mload(name)) + if eq(0x2e657468, and(0xffffffff, mload(i))) { // ".eth" + node := 1 + mstore(0x00, _ETH_NODE) + i := sub(i, 4) + let j := i + let prevNonHyphen + + // *.eth names with less than 3 characters in the 2ld are invalid + for { let end := sub(i, 3) } and(node, gt(j, end)) { j := sub(j, 1) } { + let c := and(mload(j), 0xff) + // check if `j` is the boundary of a component (`.`) + switch eq(c, 0x2e) + case 0 { + // `-` is treated specially to forbid punycode and leading/trailing `-` + switch eq(c, 0x2d) + case 0 { + // check that `c` is in [-0-9a-z] + switch or(and(gt(c, 0x2f), lt(c, 0x3a)), and(gt(c, 0x60), lt(c, 0x7b))) + case 0 { node := 0 } + default { prevNonHyphen := 1 } + } + default { + // forbid `--` (punycode) and `-` at the end of a component + switch prevNonHyphen + case 0 { node := 0 } + default { prevNonHyphen := 0 } + } + } + default { + // forbid `.` in position -7 through -5 of the name + node := 0 + } + } + + // subsequent components of the name are only required to be nonempty + for {} and(node, gt(j, name)) { j := sub(j, 1) } { + let c := and(mload(j), 0xff) + // check if `j` is the boundary of a component (`.`) + switch eq(c, 0x2e) + case 0 { + // `-` is treated specially to forbid punycode and leading/trailing `-` + switch eq(c, 0x2d) + case 0 { + // check that `c` is in [-0-9a-z] + switch or(and(gt(c, 0x2f), lt(c, 0x3a)), and(gt(c, 0x60), lt(c, 0x7b))) + case 0 { node := 0 } + default { prevNonHyphen := 1 } + } + default { + // forbid `--` (punycode) and `-` at the end of a component + switch prevNonHyphen + case 0 { node := 0 } + default { prevNonHyphen := 0 } + } + } + default { + // forbid empty and `-` at the beginning of a component + switch and(prevNonHyphen, iszero(eq(j, i))) + case 0 { node := 0 } + default { + // namehash + mstore(0x20, keccak256(add(j, 0x20), sub(i, j))) + mstore(0x00, keccak256(0x00, 0x40)) + + i := sub(j, 1) + prevNonHyphen := 0 + } + } + } + + // namehash the final component + if node { + switch and(prevNonHyphen, iszero(eq(j, i))) + case 0 { node := 0 } + default { + mstore(0x20, keccak256(add(j, 0x20), sub(i, j))) + node := keccak256(0x00, 0x40) + } + } + } + } + } + + function _nameHash(string memory name, bytes32 root) private pure returns (bytes32 node) { + assembly ("memory-safe") { + node := 1 + mstore(0x00, root) + let i := add(name, mload(name)) + let j := i + let prevNonHyphen + + for {} and(node, gt(j, name)) { j := sub(j, 1) } { + let c := and(mload(j), 0xff) + // check if `j` is the boundary of a component (`.`) + switch eq(c, 0x2e) + case 0 { + // `-` is treated specially to forbid punycode and leading/trailing `-` + switch eq(c, 0x2d) + case 0 { + // check that `c` is in [-0-9a-z] + switch or(and(gt(c, 0x2f), lt(c, 0x3a)), and(gt(c, 0x60), lt(c, 0x7b))) + case 0 { node := 0 } + default { prevNonHyphen := 1 } + } + default { + // forbid `--` (punycode) and `-` at the end of a component + switch prevNonHyphen + case 0 { node := 0 } + default { prevNonHyphen := 0 } + } + } + default { + // forbid empty and `-` at the beginning of a component + switch and(prevNonHyphen, iszero(eq(j, i))) + case 0 { node := 0 } + default { + // namehash + mstore(0x20, keccak256(add(j, 0x20), sub(i, j))) + mstore(0x00, keccak256(0x00, 0x40)) + + i := sub(j, 1) + prevNonHyphen := 0 + } + } + } + + // namehash the final component + if node { + switch and(prevNonHyphen, iszero(eq(j, i))) + case 0 { node := 0 } + default { + mstore(0x20, keccak256(add(j, 0x20), sub(i, j))) + node := keccak256(0x00, 0x40) + } + } + } + } + + function toNode(string memory name) internal pure returns (bytes32 node) { + node = _nameHash(name); + if (node == bytes32(0)) { + revert InvalidEthName(name); + } + } + + function toNode(string memory name, bytes32 root) internal pure returns (bytes32 node) { + node = _nameHash(name, root); + if (node == bytes32(0)) { + revert InvalidEthName(name); + } + } + + function toNode(address addr) internal view returns (bytes32 node, string memory name) { + // get the address's reverse name and namehash it + node = reverseNode(addr); + + // get the name that is set as the reverse record for the node + { + Resolver resolver = _REGISTRY.resolver(node); + // The default reverse resolver doesn't support ERC165, in opposition to + // the standard + if (!address(resolver).supportsInterface(Resolver.name.selector)) { + revert NoReverseResolver(addr); + } + name = resolver.name(node); + if (bytes(name).length == 0) { + revert NoReverseRecord(addr, address(resolver)); + } + } + + // check and namehash + node = _nameHash(name); + + // check that the reverse-resolved node forward-resolves to the original + // address + if (node == bytes32(0)) { + revert ENSRoot(); + } + { + address forward = toAddr(node); + if (forward != addr) { + revert ReverseForwardMismatch(addr, node, forward); + } + } + } +} diff --git a/src/utils/ERC165Checker.sol b/src/utils/ERC165Checker.sol new file mode 100644 index 00000000..647994c8 --- /dev/null +++ b/src/utils/ERC165Checker.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {IERC165} from "@forge-std/interfaces/IERC165.sol"; +import {CallWithGas} from "./CallWithGas.sol"; + +library ERC165Checker { + using CallWithGas for address; + + /** + * @notice Query if a contract implements an interface, does not check ERC165 + * support + * @param account The address of the contract to query for support of an interface + * @param interfaceId The interface identifier, as specified in ERC165 + * @return true if the contract at account indicates support of the interface with + * identifier interfaceId, false otherwise + * @dev Assumes that account contains a contract that supports ERC165, otherwise the + * behavior of this method is undefined. This precondition can be checked with + * `supportsERC165`. + */ + function _supportsERC165Interface(address account, bytes4 interfaceId) private view returns (bool) { + bytes memory data = abi.encodeCall(IERC165(account).supportsInterface, (interfaceId)); + (bool success, bytes memory result) = account.functionStaticCallWithGas(data, 30_000, 32); + return success && result.length >= 32 && bytes32(result) == bytes32(uint256(1)); + } + + function supportsERC165(address account) internal view returns (bool) { + return _supportsERC165Interface(account, type(IERC165).interfaceId) + && !_supportsERC165Interface(account, bytes4(0xffffffff)); + } + + function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) { + return supportsERC165(account) && _supportsERC165Interface(account, interfaceId); + } +} diff --git a/src/utils/UnsafeMath.sol b/src/utils/UnsafeMath.sol index 463af995..85a8a23d 100644 --- a/src/utils/UnsafeMath.sol +++ b/src/utils/UnsafeMath.sol @@ -26,6 +26,18 @@ library UnsafeMath { } } + function unsafeAdd(uint256 x, uint256 y) internal pure returns (uint256) { + unchecked { + return x + y; + } + } + + function unsafeSub(uint256 x, uint256 y) internal pure returns (uint256) { + unchecked { + return x - y; + } + } + function unsafeNeg(int256 x) internal pure returns (int256) { unchecked { return -x;