Platform: Code4rena
Start Date: 14/04/2023
Pot Size: $90,500 USDC
Total HM: 7
Participants: 59
Period: 14 days
Judge: LSDan
Total Solo HM: 3
Id: 232
League: ETH
Rank: 1/59
Findings: 3
Award: $23,845.89
🌟 Selected for report: 2
🚀 Solo Findings: 2
🌟 Selected for report: Trust
11893.0456 USDC - $11,893.05
The hexToAddress
utility parses a string into an address type.
function hexToAddress( bytes memory str, uint256 idx, uint256 lastIdx ) internal pure returns (address, bool) { if (lastIdx - idx < 40) return (address(0x0), false); (bytes32 r, bool valid) = hexStringToBytes32(str, idx, lastIdx); return (address(uint160(uint256(r))), valid); }
The issue is that in the return statement, the bytes32
type is reduced from 256 bits to 160 bits, without checking that the truncated bytes are zero. When the upper bits are not zero, the bytes32
is not a valid address and the function must return false
in the second parameter. However, it instead returns an incorrect trimmed number.
The utility is used in the flow below:
proveAndClaim _claim getOwnerAddress parseRR parseString hexToAdress
The incorrect address would be returned from _claim
and then used as the owner address for ens.setSubnodeOwner
. This would brick the node.
function proveAndClaim( bytes memory name, DNSSEC.RRSetWithSignature[] memory input ) public override { (bytes32 rootNode, bytes32 labelHash, address addr) = _claim( name, input ); ens.setSubnodeOwner(rootNode, labelHash, addr); }
Lack of validation can lead to ENS addresses becoming permanently bricked.
Manual audit
Add a check for the upper 96 bits in the highlighted return statement.
Historically, the judge has awarded medium severity to various issues which rely on some user error, are easy to check/fix and present material risk. I respect this line of thought and for the sake of consistency I believe this submission should be judged similarly.
#0 - c4-pre-sort
2023-04-30T06:55:53Z
thereksfour marked the issue as primary issue
#1 - c4-pre-sort
2023-05-01T05:45:48Z
thereksfour marked the issue as duplicate of #198
#2 - c4-judge
2023-05-08T13:07:08Z
dmvt marked the issue as satisfactory
#3 - trust1995
2023-05-11T08:28:23Z
I strongly believe this issue is incorrectly dupped to #198. This one talks about the trimming that occurs which leads to accepting invalid addresses that are in 20-byte format. #198 discusses a lack of <= 20 bytes check, which would not solve this issue. Either they are both medium severity, or they both aren't.
#4 - dmvt
2023-05-12T12:36:29Z
Agreed. They are adjacent but have different impacts and fixes.
#5 - c4-judge
2023-05-12T12:36:34Z
dmvt marked the issue as not a duplicate
#6 - c4-judge
2023-05-12T12:36:38Z
dmvt marked the issue as selected for report
#7 - c4-sponsor
2023-06-01T08:25:28Z
Arachnid marked the issue as sponsor disputed
#8 - Arachnid
2023-06-01T08:26:16Z
The upshot of this is that if you provide an invalid address in DNS, it will be set to a (possibly different) invalid address in ENS. This can be fixed by correcting the address in DNS and calling proveAndClaim again. I would argue this should be considered minor at best.
🌟 Selected for report: Trust
11893.0456 USDC - $11,893.05
In OffchainDNSResolver, resolveCallback
parses resource records received off-chain and extracts the DNS resolver address:
// Look for a valid ENS-DNS TXT record (address dnsresolver, bytes memory context) = parseRR( iter.data, iter.rdataOffset, iter.nextOffset );
The contract supports three methods of resolution through the resolver:
query
parameter originating in resolve()
Code is below:
// If we found a valid record, try to resolve it if (dnsresolver != address(0)) { if ( IERC165(dnsresolver).supportsInterface( IExtendedDNSResolver.resolve.selector ) ) { return IExtendedDNSResolver(dnsresolver).resolve( name, query, context ); } else if ( IERC165(dnsresolver).supportsInterface( IExtendedResolver.resolve.selector ) ) { return IExtendedResolver(dnsresolver).resolve(name, query); } else { (bool ok, bytes memory ret) = address(dnsresolver) .staticcall(query); if (ok) { return ret; } else { revert CouldNotResolve(name); } } }
The issue is that a resolver could support option (3), but execution would revert prior to the query
call. The function uses supportsInterface
in an unsafe way. It should first check that the contract implements ERC165, which will guarantee the call won't revert. Dynamic resolvers are not likely in practice to implement ERC165 as there's no specific signature to support ahead of time.
Resolution with custom DNS resolvers are likely to fail.
Manual audit
Use the OZ ERC165Checker.sol
library, which addresses the issue:
function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) { // query support of both ERC165 as per the spec and support of _interfaceId return supportsERC165(account) && supportsERC165InterfaceUnchecked(account, interfaceId); }
#0 - c4-pre-sort
2023-04-30T07:01:10Z
thereksfour marked the issue as primary issue
#1 - Arachnid
2023-05-05T13:23:40Z
ERC165 support is required in order to be a valid resolver. Any resolver that doesn't support it is incorrectly implemented.
#2 - c4-sponsor
2023-05-05T13:23:45Z
Arachnid marked the issue as sponsor disputed
#3 - dmvt
2023-05-09T09:49:34Z
This is easy to protect against. Issue stands.
#4 - c4-judge
2023-05-09T09:49:46Z
dmvt marked the issue as selected for report
#5 - Arachnid
2023-05-09T14:22:02Z
There's no point building a protection for this; either way the result is a failed resolution.
#6 - dmvt
2023-05-12T12:10:05Z
The OZ implementation would guarantee that the else clause gets triggered and the error returned is understandable / sane. In this case, a very simple fix will significantly enhance the composability of the protocol and improve the experience of dev users.
#7 - Arachnid
2023-05-15T17:24:47Z
I continue to disagree this is an issue. ERC165 support is a baseline requirement for a resolver; checking it's supported is a pointless waste of gas.
#8 - IllIllI000
2023-05-17T12:39:12Z
https://github.com/code-423n4/2023-04-ens/blob/83836eff1975fb47dae6b0982afd0b00294165cf/contracts/utils/UniversalResolver.sol#L498-L510 this code shows that at least in other areas, the possibility failure is acknowledged and handled