Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@ensdomains/solsha1": "0.0.3",
"@nomicfoundation/hardhat-verify": "^2.0.4",
"@openzeppelin/contracts": "4.9.3",
"@openzeppelin/contracts-v5": "npm:@openzeppelin/contracts@5.1.0",
"@openzeppelin/contracts-v5": "npm:@openzeppelin/contracts@5.3.0",
"@unruggable/gateways": "^1.2.2",
"clones-with-immutable-args": "Arachnid/clones-with-immutable-args#feature/create2",
"dns-packet": "^5.3.0",
Expand Down Expand Up @@ -218,7 +218,7 @@

"@openzeppelin/contracts": ["@openzeppelin/[email protected]", "", {}, "sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg=="],

"@openzeppelin/contracts-v5": ["@openzeppelin/contracts@5.1.0", "", {}, "sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA=="],
"@openzeppelin/contracts-v5": ["@openzeppelin/contracts@5.3.0", "", {}, "sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA=="],

"@peculiar/asn1-schema": ["@peculiar/[email protected]", "", { "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w=="],

Expand Down
197 changes: 197 additions & 0 deletions contracts/resolvers/FallbackResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.13;

import {ERC165} from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol";
import {Clones} from "@openzeppelin/contracts-v5/proxy/Clones.sol";

import {CCIPReader} from "../ccipRead/CCIPReader.sol";
import {ResolverCaller} from "../universalResolver/ResolverCaller.sol";
import {IGatewayProvider} from "../ccipRead/IGatewayProvider.sol";
import {BytesUtils} from "../utils/BytesUtils.sol";
import {IERC7996} from "../utils/IERC7996.sol";
import {ResolverFeatures} from "./ResolverFeatures.sol";

// resolver profiles
import {IExtendedResolver} from "./profiles/IExtendedResolver.sol";
import {IMulticallable} from "./IMulticallable.sol";

contract FallbackResolver is
ERC165,
IERC7996,
ResolverCaller,
IExtendedResolver
{
IGatewayProvider public immutable batchGatewayProvider;

event Deployed(address);

constructor(
IGatewayProvider _batchGatewayProvider
) CCIPReader(DEFAULT_UNSAFE_CALL_GAS) {
batchGatewayProvider = _batchGatewayProvider;
}

function deploy(
address[] memory _resolvers
) external returns (FallbackResolver) {
bytes10 prefix;
address impl;
assembly {
extcodecopy(address(), 0, 0, 40)
prefix := mload(0)
impl := shr(96, mload(10))
}
// check if we're the clone (ERC-1167)
if (prefix == bytes10(0x363d3d373d3d3d363d73)) {
return FallbackResolver(impl).deploy(_resolvers);
}
address clone = Clones.cloneWithImmutableArgs(
address(this),
abi.encode(_resolvers)
);
emit Deployed(clone);
return FallbackResolver(clone);
}

function resolvers() public view returns (address[] memory) {
return abi.decode(Clones.fetchCloneArgs(address(this)), (address[]));
}

/// @inheritdoc ERC165
function supportsInterface(
bytes4 interfaceId
) public view override(ERC165) returns (bool) {
return
interfaceId == type(IExtendedResolver).interfaceId ||
interfaceId == type(IERC7996).interfaceId ||
super.supportsInterface(interfaceId);
}

/// @inheritdoc IERC7996
function supportsFeature(bytes4 featureId) external pure returns (bool) {
return featureId == ResolverFeatures.RESOLVE_MULTICALL;
}

struct State {
uint256 resolverIndex;
bytes name;
bool multi;
bytes[] calls;
bytes[] answers;
}

/// @inheritdoc IExtendedResolver
function resolve(
bytes memory name,
bytes calldata data
) external view returns (bytes memory) {
State memory state;
state.name = name;
if (bytes4(data) == IMulticallable.multicall.selector) {
state.multi = true;
state.calls = abi.decode(data[4:], (bytes[]));
} else {
state.calls = new bytes[](1);
state.calls[0] = data;
}
state.answers = new bytes[](state.calls.length);
return _callNext(state);
}

function _callNext(
State memory state
) internal view returns (bytes memory) {
address[] memory v = resolvers();
if (state.resolverIndex >= v.length) {
bool answered;
for (uint256 i; i < state.answers.length; i++) {
if (state.answers[i].length > 0) {
answered = true;
break;
}
}
if (answered) {
if (state.multi) {
return abi.encode(state.answers);
} else {
return state.answers[0];
}
} else {
revert UnreachableName(state.name);
}
}
ccipRead(
address(this),
abi.encodeCall(
this.callResolver,
(
v[state.resolverIndex++],
state.name,
state.multi
? abi.encodeCall(
IMulticallable.multicall,
(state.calls)
)
: state.calls[0],
batchGatewayProvider.gateways()
)
),
this.resolveCallback.selector,
this.resolveCallbackError.selector,
abi.encode(state)
);
}

function resolveCallback(
bytes calldata response,
bytes calldata extraData
) external view returns (bytes memory) {
bytes memory unwrapped = abi.decode(response, (bytes));
State memory state = abi.decode(extraData, (State));
if (state.multi) {
bytes[] memory m = abi.decode(unwrapped, (bytes[]));
bytes[] memory calls = state.calls;
bytes[] memory answers = state.answers;
uint256 need;
uint256 next;
if (m.length == calls.length) {
for (uint256 i; i < m.length; i++) {
while (answers[next].length > 0) {
next++;
}
if (_isNullAnswer(m[i])) {
calls[need++] = calls[i];
} else {
answers[next] = m[i];
}
++next;
}
if (need == 0) {
return abi.encode(state.answers);
}
assembly {
mstore(calls, need) // truncate
}
}
} else if (_isNullAnswer(unwrapped)) {
state.answers[0] = unwrapped;
} else {
return unwrapped;
}
return _callNext(state);
}

function resolveCallbackError(
bytes calldata,
bytes calldata extraData
) external view returns (bytes memory) {
return _callNext(abi.decode(extraData, (State)));
}

function _isNullAnswer(bytes memory v) internal pure returns (bool) {
return
BytesUtils.isZeros(v) ||
keccak256(v) ==
0x569e75fc77c1a856f6daaf9e69d8a9566ca34aa47f9133711ce065a571af0cfd; // abi.encode('')
}
}
53 changes: 53 additions & 0 deletions contracts/utils/BytesUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,57 @@ library BytesUtils {
}
return type(uint256).max;
}

/// @dev Determine if `v` is all zeros.
/// @param v The bytes to search.
/// @return `true` if all zeros.
function isZeros(bytes memory v) internal pure returns (bool) {
uint256 ptr;
assembly {
ptr := add(v, 32)
}
return unsafeIsZeros(ptr, v.length);
}

/// @dev Determine if `v[off:len]` is all zeros.
/// @param v The bytes to search.
/// @param off The offset to start searching.
/// @param len The number of bytes to search.
/// @return `true` if all zeros.
function isZeros(
bytes memory v,
uint256 off,
uint256 len
) internal pure returns (bool) {
_checkBound(v, off + len);
uint256 ptr;
assembly {
ptr := add(v, 32)
}
return unsafeIsZeros(ptr + off, len);
}

/// @dev Determine if `mem[ptr:ptr+len]` is all zeros.
function unsafeIsZeros(
uint256 ptr,
uint256 len
) internal pure returns (bool ret) {
assembly {
let end := add(ptr, len)
ret := 1 // assume null
// prettier-ignore
for {} lt(ptr, end) {} { // while (ptr < end)
let x := mload(ptr) // remember last
ptr := add(ptr, 32) // step by word
if x {
ret := 0 // nonzero
if gt(ptr, end) {
// overshot, so shift and recheck
ret := iszero(shr(shl(3, sub(ptr, end)), x))
}
break
}
}
}
}
}
Loading