From 6db64440250bf008d886bd8b37833d7f6498e675 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Thu, 15 Feb 2024 15:37:30 +0000 Subject: [PATCH 01/11] Add hexToBytes() --- .../profiles/ExtendedDNSResolver.sol | 35 +- contracts/utils/HexUtils.sol | 24 +- contracts/utils/TestHexUtils.sol | 8 + deploy/dnsregistrar/20_set_tlds.ts | 1317 +---------------- test/utils/TestHexUtils.js | 57 +- 5 files changed, 141 insertions(+), 1300 deletions(-) diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index bdb942fe..3b035c4f 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -27,21 +27,28 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { bytes calldata context ) external pure override returns (bytes memory) { bytes4 selector = bytes4(data); - if ( - selector == IAddrResolver.addr.selector || - selector == IAddressResolver.addr.selector - ) { - if (selector == IAddressResolver.addr.selector) { - (, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256)); - if (coinType != COIN_TYPE_ETH) return abi.encode(""); - } - (address record, bool valid) = context.hexToAddress( - 2, - context.length - ); - if (!valid) revert InvalidAddressFormat(); - return abi.encode(record); + if (selector == IAddrResolver.addr.selector) { + return _resolveAddr(data, context); + } else if (selector == IAddressResolver.addr.selector) { + return _resolveAddress(data, context); } revert NotImplemented(); } + + function _resolveAddress( + bytes calldata data, + bytes calldata context + ) internal pure returns (bytes memory) { + (, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256)); + (address record, bool valid) = context.hexToAddress(2, context.length); + if (!valid) revert InvalidAddressFormat(); + return abi.encode(record); + } + + function _resolveAddr( + bytes calldata data, + bytes calldata context + ) internal pure returns (bytes memory) { + return _resolveAddress(data, context); + } } diff --git a/contracts/utils/HexUtils.sol b/contracts/utils/HexUtils.sol index 7cb3a1e0..35ad52d0 100644 --- a/contracts/utils/HexUtils.sol +++ b/contracts/utils/HexUtils.sol @@ -12,11 +12,29 @@ library HexUtils { bytes memory str, uint256 idx, uint256 lastIdx - ) internal pure returns (bytes32 r, bool valid) { + ) internal pure returns (bytes32, bool) { + require(lastIdx - idx <= 64); + (bytes memory r, bool valid) = hexToBytes(str, idx, lastIdx); + if (!valid) { + return (bytes32(0), false); + } + bytes32 ret; + assembly { + ret := shr(mul(4, sub(64, sub(lastIdx, idx))), mload(add(r, 32))) + } + return (ret, true); + } + + function hexToBytes( + bytes memory str, + uint256 idx, + uint256 lastIdx + ) internal pure returns (bytes memory r, bool valid) { uint256 hexLength = lastIdx - idx; - if ((hexLength != 64 && hexLength != 40) || hexLength % 2 == 1) { + if (hexLength % 2 == 1) { revert("Invalid string length"); } + r = new bytes(hexLength / 2); valid = true; assembly { // check that the index to read to is not past the end of the string @@ -58,7 +76,7 @@ library HexUtils { break } let combined := or(shl(4, byte1), byte2) - r := or(shl(8, r), combined) + mstore8(add(add(r, 32), div(sub(i, idx), 2)), combined) } } } diff --git a/contracts/utils/TestHexUtils.sol b/contracts/utils/TestHexUtils.sol index 6dd9088e..b6624f09 100644 --- a/contracts/utils/TestHexUtils.sol +++ b/contracts/utils/TestHexUtils.sol @@ -6,6 +6,14 @@ import {HexUtils} from "./HexUtils.sol"; contract TestHexUtils { using HexUtils for *; + function hexToBytes( + bytes calldata name, + uint256 idx, + uint256 lastInx + ) public pure returns (bytes memory, bool) { + return name.hexToBytes(idx, lastInx); + } + function hexStringToBytes32( bytes calldata name, uint256 idx, diff --git a/deploy/dnsregistrar/20_set_tlds.ts b/deploy/dnsregistrar/20_set_tlds.ts index abd57208..fa6fc2f7 100644 --- a/deploy/dnsregistrar/20_set_tlds.ts +++ b/deploy/dnsregistrar/20_set_tlds.ts @@ -1,1291 +1,64 @@ -import packet from 'dns-packet' +import { namehash } from 'ethers/lib/utils' import { ethers } from 'hardhat' +import packet from 'dns-packet' import { DeployFunction } from 'hardhat-deploy/types' import { HardhatRuntimeEnvironment } from 'hardhat/types' -const fullMap = [ - 'exposed', - 'target', - 'rocher', - 'weatherchannel', - 'au', - 'homes', - 'bostik', - 'mini', - 'wien', - 'ladbrokes', - 'boutique', - 'democrat', - 'tci', - 'haus', - 'gap', - 'cal', - 'safety', - 'camera', - 'pe', - 'claims', - 'spreadbetting', - 'neustar', - 'dentist', - 'boston', - 'tm', - 'band', - 'nc', - 'toshiba', - 'statefarm', - 'imamat', - 'vistaprint', - 'kr', - 'rwe', - 'macys', - 'thd', - 'bt', - 'ren', - 'email', - 'maison', - 'fish', - 'ar', - 'lt', - 'sfr', - 'locker', - 'map', - 'sa', - 'mx', - 'nexus', - 'read', - 'co', - 'lancome', - 'game', - 'rightathome', - 'sydney', - 'press', - 'duns', - 'pictures', - 'nowruz', - 'smart', - 'nfl', - 'barclaycard', - 'sexy', - 'gifts', - 'wolterskluwer', - 'degree', - 'comsec', - 'aig', - 'abb', - 'my', - 'af', - 'edeka', - 'education', - 'surgery', - 'spot', - 'th', - 'marketing', - 'akdn', - 'link', - 'university', - 'tw', - 'iveco', - 'site', - 'booking', - 'solar', - 'ie', - 'organic', - 'pid', - 'training', - 'diet', - 'hn', - 'philips', - 'catholic', - 'godaddy', - 'lancia', - 'moscow', - 'vc', - 'kerryhotels', - 'se', - 'dev', - 'holiday', - 'kg', - 'unicom', - 'tjx', - 'gmo', - 'hdfc', - 'stcgroup', - 'online', - 'microsoft', - 'weir', - 'capital', - 'parts', - 'gbiz', - 'playstation', - 'bharti', - 'hughes', - 'piaget', - 'trade', - 'gle', - 'reit', - 'hbo', - 'caravan', - 'qpon', - 'you', - 'forsale', - 'tunes', - 'yahoo', - 'rugby', - 'fun', - 'arpa', - 'drive', - 'seven', - 'xerox', - 'work', - 'loan', - 'mobily', - 'lv', - 'shopping', - 'charity', - 'is', - 'dodge', - 'eco', - 'aws', - 'rocks', - 'metlife', - 'fyi', - 'wiki', - 'business', - 'whoswho', - 'sh', - 'nextdirect', - 'bot', - 'bmw', - 'lplfinancial', - 'iselect', - 'gent', - 'ericsson', - 'staples', - 'courses', - 'goo', - 'limo', - 'pfizer', - 'az', - 'office', - 'lixil', - 'fox', - 'coach', - 'futbol', - 'airbus', - 'tl', - 'richardli', - 'got', - 'zone', - 'news', - 'delivery', - 'imdb', - 'landrover', - 'orange', - 'weather', - 'hockey', - 'help', - 'aarp', - 'fresenius', - 'jio', - 'deal', - 'ma', - 'hgtv', - 'goog', - 'ac', - 'foodnetwork', - 'run', - 'lifestyle', - 'bloomberg', - 'ltd', - 'tax', - 'guide', - 'tips', - 'fire', - 'gl', - 'buy', - 'games', - 'travelersinsurance', - 'network', - 'anz', - 'play', - 'onyourside', - 'taobao', - 'politie', - 'mlb', - 'fi', - 'hiv', - 'schmidt', - 'homegoods', - 'vlaanderen', - 'leclerc', - 'immobilien', - 'bond', - 'ceb', - 'software', - 'toray', - 'select', - 'mortgage', - 'msd', - 'lipsy', - 'flir', - 'hot', - 'bentley', - 'hosting', - 'photo', - 'hiphop', - 'gea', - 'amfam', - 'asia', - 'versicherung', - 'eu', - 'hospital', - 'lol', - 'lease', - 'name', - 'estate', - 'one', - 'flights', - 'sncf', - 'origins', - 'cloud', - 'jpmorgan', - 'church', - 'jnj', - 'gucci', - 'cooking', - 'cartier', - 'commbank', - 'llc', - 'reviews', - 'creditcard', - 'bofa', - 'host', - 'de', - 'next', - 'emerck', - 'su', - 'hyatt', - 'dance', - 'goldpoint', - 'fit', - 'yodobashi', - 'lc', - 'bz', - 'edu', - 'pohl', - 'construction', - 'windows', - 'esurance', - 'ki', - 'baby', - 'wanggou', - 'house', - 'swiftcover', - 'airforce', - 'vodka', - 'institute', - 'careers', - 'ieee', - 'lgbt', - 'kinder', - 'hitachi', - 'racing', - 'tv', - 'industries', - 'boats', - 'ollo', - 'hangout', - 'itv', - 'limited', - 'archi', - 'google', - 'rogers', - 'army', - 'events', - 'download', - 'schwarz', - 'kim', - 'diy', - 'citi', - 'accountant', - 'ismaili', - 'singles', - 'mutual', - 'shell', - 'cc', - 'norton', - 'review', - 'design', - 'dot', - 'irish', - 'guru', - 'lancaster', - 'academy', - 'rentals', - 'holdings', - 'gift', - 'omega', - 'pwc', - 'blackfriday', - 'legal', - 'realestate', - 'redumbrella', - 'gmail', - 'dvag', - 'guge', - 'beats', - 'protection', - 'tatar', - 'kindle', - 'cfd', - 'consulting', - 'fiat', - 'extraspace', - 'weibo', - 'kuokgroup', - 'cuisinella', - 'audi', - 'americanexpress', - 'xxx', - 'team', - 'everbank', - 'tf', - 'toyota', - 'land', - 'sener', - 'tvs', - 'travelers', - 'shangrila', - 'durban', - 'camp', - 'scjohnson', - 'oracle', - 'markets', - 'investments', - 'watches', - 'williamhill', - 'kiwi', - 'dclk', - 'boehringer', - 'tattoo', - 'dvr', - 'hr', - 'bing', - 'viva', - 'xihuan', - 'mopar', - 'ryukyu', - 'chrysler', - 'town', - 'pioneer', - 'sew', - 'tkmaxx', - 'jewelry', - 'lotto', - 'pl', - 'srl', - 'vivo', - 'tjmaxx', - 'film', - 'sn', - 'nadex', - 'verisign', - 'analytics', - 'wedding', - 'loans', - 'broker', - 'expert', - 'barclays', - 'wtf', - 'technology', - 'nowtv', - 'taxi', - 'fund', - 'cruise', - 'natura', - 'engineer', - 'caseih', - 'ally', - 'sling', - 'viking', - 'abogado', - 're', - 'nyc', - 'nokia', - 'food', - 'dell', - 'si', - 'bridgestone', - 'lpl', - 'nagoya', - 'accountants', - 'art', - 'bayern', - 'vegas', - 'esq', - 'il', - 'lotte', - 'monster', - 'lamborghini', - 'aeg', - 'icbc', - 'engineering', - 'us', - 'silk', - 'osaka', - 'mn', - 'samsclub', - 'webcam', - 'makeup', - 'juegos', - 'walmart', - 'lamer', - 'yachts', - 'abarth', - 'nz', - 'studio', - 'farm', - 'mba', - 'med', - 'fr', - 'wang', - 'dk', - 'top', - 'sanofi', - 'moda', - 'golf', - 'jll', - 'cafe', - 'bike', - 'grocery', - 'financial', - 'free', - 'law', - 'forex', - 'prof', - 'miami', - 'pet', - 'ag', - 'bg', - 'yokohama', - 'info', - 'yamaxun', - 'win', - 'london', - 'lefrak', - 'allstate', - 'ventures', - 'ntt', - 'gs', - 'delta', - 'center', - 'corsica', - 'tatamotors', - 'walter', - 'helsinki', - 'tires', - 'reise', - 'eat', - 'talk', - 'afamilycompany', - 'gratis', - 'lawyer', - 'pars', - 'alfaromeo', - 'visa', - 'auction', - 'icu', - 'chrome', - 'inc', - 'report', - 'hyundai', - 'pw', - 'box', - 'lighting', - 'xin', - 'room', - 'day', - 'fage', - 'aetna', - 'digital', - 'genting', - 'exchange', - 'ong', - 'services', - 'pharmacy', - 'yun', - 'fedex', - 'ceo', - 'cricket', - 'cbre', - 'voting', - 'cleaning', - 'sharp', - 'pics', - 'biz', - 'able', - 'soccer', - 'scholarships', - 'build', - 'cruises', - 'ferrari', - 'komatsu', - 'giving', - 'asda', - 'rent', - 'bbc', - 'volkswagen', - 'starhub', - 'fast', - 'off', - 'memorial', - 'monash', - 'tt', - 'vision', - 'sb', - 'associates', - 'management', - 'coffee', - 'soy', - 'csc', - 'beer', - 'dunlop', - 'frontier', - 'channel', - 'glass', - 'uy', - 'pramerica', - 'tienda', - 'otsuka', - 'wme', - 'vacations', - 'sc', - 'post', - 'lundbeck', - 'cityeats', - 'casa', - 'obi', - 'discover', - 'xbox', - 'dental', - 'brussels', - 'grainger', - 'gallo', - 'cyou', - 'wales', - 'money', - 'oldnavy', - 'community', - 'builders', - 'chanel', - 'kddi', - 'barefoot', - 'coop', - 'forum', - 'raid', - 'arab', - 'fishing', - 'nissay', - 'warman', - 'shaw', - 'uk', - 'datsun', - 'bank', - 'like', - 'meme', - 'teva', - 'dupont', - 'auto', - 'latrobe', - 'ink', - 'diamonds', - 'bzh', - 'party', - 'redstone', - 'nec', - 'softbank', - 'aquarelle', - 'care', - 'aw', - 'property', - 'cards', - 'pro', - 'earth', - 'vin', - 'quest', - 'creditunion', - 'cisco', - 'frogans', - 'sandvik', - 'adult', - 'lu', - 'statebank', - 'menu', - 'global', - 'okinawa', - 'realty', - 'computer', - 'tours', - 'poker', - 'skin', - 'budapest', - 'finance', - 'srt', - 'phd', - 'zero', - 'northwesternmutual', - 'recipes', - 'duck', - 'tab', - 'shia', - 'world', - 'ninja', - 'by', - 'seek', - 'show', - 'pay', - 'gop', - 'latino', - 'repair', - 'salon', - 'maif', - 'author', - 'olayan', - 'calvinklein', - 'tui', - 'sony', - 'lk', - 'star', - 'wtc', - 'clinique', - 'kpn', - 'vanguard', - 'chintai', - 'gr', - 'lupin', - 'company', - 'amex', - 'case', - 'lifeinsurance', - 'career', - 'plumbing', - 'ott', - 'school', - 'deloitte', - 'broadway', - 'sandvikcoromant', - 'insure', - 'shriram', - 'sakura', - 'study', - 'bbt', - 'casino', - 'motorcycles', - 'healthcare', - 'black', - 'in', - 'hotels', - 'aramco', - 'youtube', - 'safe', - 'place', - 'ibm', - 'zip', - 'honeywell', - 'domains', - 'ggee', - 'support', - 'pink', - 'capitalone', - 'weber', - 'website', - 'dating', - 'aaa', - 'scb', - 'mov', - 'tmall', - 'sca', - 'rsvp', - 'es', - 'attorney', - 'red', - 'vig', - 'flickr', - 'boo', - 'firmdale', - 'stc', - 'aol', - 'symantec', - 'ricoh', - 'storage', - 'hdfcbank', - 'newholland', - 'marriott', - 'pt', - 'moto', - 'am', - 'ke', - 'pccw', - 'wow', - 'contractors', - 'moe', - 'vet', - 'ferrero', - 'nba', - 'voyage', - 'farmers', - 'restaurant', - 'jprs', - 'tube', - 'observer', - 'ubs', - 'surf', - 'hu', - 'gmbh', - 'ril', - 'moi', - 'mtr', - 'hoteles', - 'amica', - 'baidu', - 'actor', - 'dabur', - 'sina', - 'nab', - 'theatre', - 'la', - 'hamburg', - 'photos', - 'ky', - 'kyoto', - 'energy', - 'travelchannel', - 'gi', - 'meet', - 'condos', - 'fly', - 'kpmg', - 'feedback', - 'dhl', - 'sx', - 'schule', - 'supply', - 'abbott', - 'chase', - 'int', - 'ro', - 'zara', - 'guitars', - 'rexroth', - 'intuit', - 'reverse', - 'bms', - 'car', - 'bm', - 'graphics', - 'org', - 'space', - 'jcb', - 'live', - 'nationwide', - 'bar', - 'cx', - 'lanxess', - 'juniper', - 'loft', - 'media', - 'secure', - 'voto', - 'data', - 'codes', - 'mobi', - 'cheap', - 'ftr', - 'furniture', - 'clubmed', - 'international', - 'tushu', - 'gives', - 'productions', - 'buzz', - 'bestbuy', - 'sale', - 'ipiranga', - 'new', - 'xyz', - 'afl', - 'skype', - 'blue', - 'george', - 'lb', - 'sbi', - 'pin', - 'lego', - 'cl', - 'guardian', - 'com', - 'avianca', - 'pm', - 'catering', - 'hsbc', - 'bosch', - 'systems', - 'country', - 'airtel', - 'mattel', - 'cam', - 'dnp', - 'abbvie', - 'frl', - 'reisen', - 'kfh', - 'lasalle', - 'supplies', - 'movie', - 'rip', - 'tech', - 'shoes', - 'frontdoor', - 'gold', - 'sohu', - 'taipei', - 'docs', - 'comcast', - 'audio', - 'lr', - 'passagens', - 'io', - 'uno', - 'cba', - 'circle', - 'agency', - 'tel', - 'viajes', - 'jot', - 'schaeffler', - 'android', - 'travel', - 'bible', - 'tn', - 'nf', - 'luxe', - 'yandex', - 'watch', - 'hotmail', - 'rich', - 'alipay', - 'style', - 'vote', - 'axa', - 'luxury', - 'desi', - 'book', - 'ug', - 'qvc', - 'cr', - 'prudential', - 'wed', - 'science', - 'ru', - 'shop', - 'cipriani', - 'gov', - 'blog', - 'horse', - 'cool', - 'dds', - 'ubank', - 'uconnect', - 'compare', - 'tennis', - 'intel', - 'cfa', - 'ping', - 'alsace', - 'mint', - 'showtime', - 'kitchen', - 'coupon', - 'temasek', - 'click', - 'social', - 'melbourne', - 'how', - 'fan', - 'fans', - 'nra', - 'jaguar', - 'mitsubishi', - 'save', - 'sky', - 'accenture', - 'yt', - 'gd', - 'vip', - 'open', - 'jobs', - 'bid', - 'ses', - 'aco', - 'discount', - 'bingo', - 'mom', - 'shiksha', - 'joburg', - 'kerryproperties', - 'nico', - 'best', - 'pizza', - 'krd', - 'toys', - 'mit', - 'zuerich', - 'tiffany', - 'cbs', - 'woodside', - 'pnc', - 'partners', - 'immo', - 'bradesco', - 'ist', - 'fashion', - 'onl', - 'auspost', - 'ca', - 'call', - 'yoga', - 'fo', - 'winners', - 'stream', - 'bw', - 'berlin', - 'lds', - 'fujitsu', - 'football', - 'citadel', - 'fail', - 'express', - 'cab', - 'glade', - 'works', - 'wf', - 'honda', - 'swatch', - 'rest', - 'hkt', - 'homesense', - 'citic', - 'song', - 'lincoln', - 'college', - 'video', - 'dog', - 'cancerresearch', - 'pr', - 'rmit', - 'epson', - 'cbn', - 'total', - 'today', - 'ice', - 'deals', - 'athleta', - 'ngo', - 'theater', - 'brother', - 'love', - 'gallup', - 'progressive', - 'ltda', - 'dubai', - 'na', - 'christmas', - 'kosher', - 'bcg', - 'store', - 'ups', - 'tiaa', - 'search', - 'ws', - 'basketball', - 'security', - 'baseball', - 'club', - 'fido', - 'hisamitsu', - 'kaufen', - 'stada', - 'insurance', - 'flowers', - 'locus', - 'dad', - 'net', - 'apple', - 'bbva', - 'sarl', - 'kerrylogistics', - 'blockbuster', - 'africa', - 'vana', - 'date', - 'mckinsey', - 'doha', - 'rehab', - 'lilly', - 'ad', - 'alibaba', - 'olayangroup', - 'ee', - 'dtv', - 'vuelos', - 'cymru', - 'ax', - 'azure', - 'villas', - 'mtn', - 'family', - 'reliance', - 'mma', - 'agakhan', - 'cern', - 'bio', - 'samsung', - 'marshalls', - 'shouji', - 'bugatti', - 'garden', - 'solutions', - 'hermes', - 'ads', - 'ooo', - 'enterprises', - 'cash', - 'volvo', - 'bananarepublic', - 'fairwinds', - 'kia', - 'canon', - 'zappos', - 'sucks', - 'amsterdam', - 'trust', - 'adac', - 'xfinity', - 'city', - 'fitness', - 'mil', - 'health', - 'sbs', - 'sg', - 'ski', - 'mm', - 'faith', - 'doctor', - 'id', - 'jetzt', - 'jmp', - 'infiniti', - 'gn', - 'here', - 'now', - 'living', - 'bnpparibas', - 'za', - 'porn', - 'tokyo', - 'green', - 'rodeo', - 'museum', - 'smile', - 'itau', - 'promo', - 'zm', - 'goodyear', - 'wine', - 'beauty', - 'no', - 'linde', - 'be', - 'vn', - 'merckmsd', - 'pictet', - 'life', - 'tirol', - 'foo', - 'gallery', - 'contact', - 'photography', - 'banamex', - 'scor', - 'mormon', - 'istanbul', - 'firestone', - 'pub', - 'republican', - 'nhk', - 'ing', - 'etisalat', - 'plus', - 'jp', - 'praxi', - 'trv', - 'pru', - 'panasonic', - 'lidl', - 'ford', - 'aigo', - 'nike', - 'at', - 'properties', - 'capetown', - 'java', - 'abc', - 'virgin', - 'navy', - 'americanfamily', - 'sex', - 'arte', - 'coupons', - 'prime', - 'crs', - 'jcp', - 'autos', - 'abudhabi', - 'liaison', - 'apartments', - 'tools', - 'stockholm', - 'florist', - 'lexus', - 'phone', - 'sas', - 'crown', - 'tdk', - 'saarland', - 'clothing', - 'jeep', - 'paris', - 'credit', - 'realtor', - 'cn', - 'direct', - 'fujixerox', - 'maserati', - 'saxo', - 'gdn', - 'directory', - 'app', - 'fidelity', - 'anquan', - 'lat', - 'prod', - 'trading', - 'nissan', - 'homedepot', - 'gripe', - 'netflix', - 'mobile', - 'dish', - 'clinic', - 'cookingchannel', - 'men', - 'ikano', - 'equipment', - 'mls', - 'suzuki', - 'sj', - 'hk', - 'group', - 'ovh', - 'bargains', - 'tickets', - 'bet', - 'joy', - 'hair', - 'page', - 'physio', - 'chat', - 'audible', - 'dealer', - 'nikon', - 'cars', - 'bnl', - 'mg', - 'allfinanz', - 'foundation', - 'market', - 'netbank', -] - -const tld_map = { - mainnet: ['xyz'], - ropsten: ['xyz'], - localhost: ['xyz'], - hardhat: ['xyz'], - goerli: fullMap, - sepolia: ['com', 'xyz'], -} - -const ZERO_HASH = - '0x0000000000000000000000000000000000000000000000000000000000000000' - function encodeName(name: string) { return '0x' + packet.name.encode(name).toString('hex') } -async function setTLDs( - owner: string, - registry: any, - registrar: any, - tlds: any[], -) { - if (tlds === undefined) { - return [] - } +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { getNamedAccounts, deployments } = hre + const { deploy } = deployments + const { deployer, owner } = await getNamedAccounts() - const transactions: any[] = [] - for (const tld of tlds) { - if ( - registrar.address !== (await registry.owner(ethers.utils.namehash(tld))) - ) { - console.log(`Transferring .${tld} to new DNS registrar`) - transactions.push( - await registrar.enableNode(encodeName(tld), { - gasLimit: 10000000, - }), - ) + const registry = await ethers.getContract('ENSRegistry') + const publicSuffixList = await ethers.getContract('SimplePublicSuffixList') + const dnsRegistrar = await ethers.getContract('DNSRegistrar') + + const suffixList = await ( + await fetch('https://publicsuffix.org/list/public_suffix_list.dat') + ).text() + let suffixes = suffixList + .split('\n') + .filter((suffix) => !suffix.startsWith('//') && suffix.trim() != '') + console.log(`Processing ${suffixes.length} public suffixes...`) + + for (let i = 0; i < suffixes.length; i++) { + const suffix = suffixes[i] + if (!suffix.match(/^[a-z0-9]+$/)) { + continue } - } - return transactions -} -const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - const { getNamedAccounts, network } = hre - const { owner } = await getNamedAccounts() - const signer = await ethers.getSigner(owner) + const node = namehash(suffix) - console.log('Running setTLDs') + const owner = await registry.owner(node) + if (owner == dnsRegistrar.address) { + console.log(`Skipping .${suffix}; already owned`) + continue + } - let transactions: any[] = [] - const registrar = await ethers.getContract('DNSRegistrar', signer) - const registry = await ethers.getContract('ENSRegistry', signer) - transactions = await setTLDs( - owner, - registry, - registrar, - tld_map[network.name as keyof typeof tld_map], - ) + const encodedSuffix = encodeName(suffix) + if (!(await publicSuffixList.isPublicSuffix(encodedSuffix))) { + console.log(`Skipping .${suffix}; not in the PSL`) + continue + } - if (transactions.length > 0) { - console.log( - `Waiting on ${transactions.length} transactions setting DNS TLDs`, - ) - await Promise.all(transactions.map((tx) => tx.wait())) + try { + const tx = await dnsRegistrar.enableNode(encodedSuffix, { + maxFeePerGas: 25000000000, + maxPriorityFeePerGas: 1000000000, + }) + console.log(`Enabling .${suffix} (${tx.hash})...`) + await tx.wait() + } catch (e) { + console.log(`Error enabling .${suffix}: ${e.toString()}`) + } } } -func.tags = ['registrar-tlds'] -func.dependencies = ['registry', 'dnssec-oracle'] -func.runAtTheEnd = true +func.tags = ['settlds'] +func.dependencies = [] export default func diff --git a/test/utils/TestHexUtils.js b/test/utils/TestHexUtils.js index 5d39ee8e..f45d31bf 100644 --- a/test/utils/TestHexUtils.js +++ b/test/utils/TestHexUtils.js @@ -16,6 +16,46 @@ describe('HexUtils', () => { HexUtils = await HexUtilsFactory.deploy() }) + describe('hexToBytes()', () => { + it('Converts a hex string to bytes', async () => { + let [bytes32, valid] = await HexUtils.hexToBytes( + toUtf8Bytes( + '5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da', + ), + 0, + 64, + ) + expect(valid).to.equal(true) + expect(bytes32).to.equal( + '0x5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da', + ) + }) + + it('Handles short strings', async () => { + let [bytes32, valid] = await HexUtils.hexToBytes( + toUtf8Bytes('5cee'), + 0, + 4, + ) + expect(valid).to.equal(true) + expect(bytes32).to.equal('0x5cee') + }) + + it('Handles long strings', async () => { + let [bytes32, valid] = await HexUtils.hexToBytes( + toUtf8Bytes( + '5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da010203', + ), + 0, + 70, + ) + expect(valid).to.equal(true) + expect(bytes32).to.equal( + '0x5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da010203', + ) + }) + }) + describe('hexStringToBytes32()', () => { it('Converts a hex string to bytes32', async () => { let [bytes32, valid] = await HexUtils.hexStringToBytes32( @@ -25,10 +65,10 @@ describe('HexUtils', () => { 0, 64, ) + expect(valid).to.equal(true) expect(bytes32).to.equal( '0x5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da', ) - expect(valid).to.equal(true) }) it('Uses the correct index to read from', async () => { let [bytes32, valid] = await HexUtils.hexStringToBytes32( @@ -38,10 +78,10 @@ describe('HexUtils', () => { 7, 71, ) + expect(valid).to.equal(true) expect(bytes32).to.equal( '0x5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da', ) - expect(valid).to.equal(true) }) it('Correctly parses all hex characters', async () => { let [bytes32, valid] = await HexUtils.hexStringToBytes32( @@ -49,10 +89,10 @@ describe('HexUtils', () => { 0, 40, ) + expect(valid).to.equal(true) expect(bytes32).to.equal( '0x0000000000000000000000000123456789abcdefabcdef0123456789abcdefab', ) - expect(valid).to.equal(true) }) it('Returns invalid when the string contains non-hex characters', async () => { const [bytes32, valid] = await HexUtils.hexStringToBytes32( @@ -62,8 +102,8 @@ describe('HexUtils', () => { 0, 64, ) - expect(bytes32).to.equal(NULL_HASH) expect(valid).to.equal(false) + expect(bytes32).to.equal(NULL_HASH) }) it('Reverts when the string is too short', async () => { await expect( @@ -87,8 +127,8 @@ describe('HexUtils', () => { 0, 40, ) - expect(address).to.equal('0x5ceE339e13375638553bdF5a6e36BA80fB9f6a4F') expect(valid).to.equal(true) + expect(address).to.equal('0x5ceE339e13375638553bdF5a6e36BA80fB9f6a4F') }) it('Does not allow sizes smaller than 40 characters', async () => { let [address, valid] = await HexUtils.hexToAddress( @@ -98,8 +138,8 @@ describe('HexUtils', () => { 0, 39, ) - expect(address).to.equal('0x0000000000000000000000000000000000000000') expect(valid).to.equal(false) + expect(address).to.equal('0x0000000000000000000000000000000000000000') }) }) @@ -117,11 +157,6 @@ describe('HexUtils', () => { ).to.be.reverted }) - it('not enough length', async () => { - await expect(HexUtils.hexStringToBytes32(toUtf8Bytes(hex32Bytes), 0, 2)) - .to.be.reverted - }) - it('exceed length', async () => { await expect( HexUtils.hexStringToBytes32( From e95380c2a7f87c87e19c3bc8e15acdd744853799 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 19 Feb 2024 15:29:02 +0000 Subject: [PATCH 02/11] First version of better parser --- .../profiles/ExtendedDNSResolver.sol | 150 +++++++++++++++++- test/dnsregistrar/TestOffchainDNSResolver.js | 20 +-- test/resolvers/TestExtendedDNSResolver.js | 81 ++++++++++ 3 files changed, 232 insertions(+), 19 deletions(-) create mode 100644 test/resolvers/TestExtendedDNSResolver.js diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index 3b035c4f..c6fd467a 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -2,18 +2,22 @@ pragma solidity ^0.8.4; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; import "../../resolvers/profiles/IExtendedDNSResolver.sol"; import "../../resolvers/profiles/IAddressResolver.sol"; import "../../resolvers/profiles/IAddrResolver.sol"; import "../../utils/HexUtils.sol"; +import "../../dnssec-oracle/BytesUtils.sol"; contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { using HexUtils for *; + using BytesUtils for *; + using Strings for *; uint256 private constant COIN_TYPE_ETH = 60; error NotImplemented(); - error InvalidAddressFormat(); + error InvalidAddressFormat(bytes addr); function supportsInterface( bytes4 interfaceId @@ -40,15 +44,149 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { bytes calldata context ) internal pure returns (bytes memory) { (, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256)); - (address record, bool valid) = context.hexToAddress(2, context.length); - if (!valid) revert InvalidAddressFormat(); - return abi.encode(record); + bytes memory value = _findValue( + context, + bytes.concat("a[", bytes(coinType.toString()), "]=") + ); + if (value.length == 0) { + return value; + } + (bytes memory record, bool valid) = value.hexToBytes(2, value.length); + if (!valid) revert InvalidAddressFormat(value); + return record; } function _resolveAddr( - bytes calldata data, + bytes calldata /* data */, bytes calldata context ) internal pure returns (bytes memory) { - return _resolveAddress(data, context); + bytes memory value = _findValue(context, "a[60]="); + if (value.length == 0) { + return value; + } + (bytes memory record, bool valid) = value.hexToBytes(2, value.length); + if (!valid) revert InvalidAddressFormat(value); + return record; + } + + uint256 constant STATE_START = 0; + uint256 constant STATE_IGNORED_KEY = 1; + uint256 constant STATE_IGNORED_KEY_ARG = 2; + uint256 constant STATE_VALUE = 3; + uint256 constant STATE_QUOTED_VALUE = 4; + uint256 constant STATE_UNQUOTED_VALUE = 5; + uint256 constant STATE_IGNORED_VALUE = 6; + uint256 constant STATE_IGNORED_QUOTED_VALUE = 7; + uint256 constant STATE_IGNORED_UNQUOTED_VALUE = 8; + + function _findValue( + bytes memory data, + bytes memory key + ) internal pure returns (bytes memory value) { + uint256 state = STATE_START; + uint256 len = data.length; + for (uint256 i = 0; i < len; ) { + if (state == STATE_START) { + if (data.equals(i, key, 0, key.length)) { + i += key.length; + state = STATE_VALUE; + } else { + state = STATE_IGNORED_KEY; + } + } else if (state == STATE_IGNORED_KEY) { + for (; i < len; i++) { + if (data[i] == "=") { + state = STATE_IGNORED_VALUE; + i += 1; + break; + } else if (data[i] == "[") { + state = STATE_IGNORED_KEY_ARG; + i += 1; + break; + } + } + } else if (state == STATE_IGNORED_KEY_ARG) { + for (; i < len; i++) { + if (data[i] == "]") { + state = STATE_IGNORED_VALUE; + i += 1; + if (data[i] == "=") { + i += 1; + } + break; + } + } + } else if (state == STATE_VALUE) { + if (data[i] == "'") { + state = STATE_QUOTED_VALUE; + i += 1; + } else { + state = STATE_UNQUOTED_VALUE; + } + } else if (state == STATE_QUOTED_VALUE) { + uint256 start = i; + uint256 valueLen = 0; + bool escaped = false; + for (; i < len; i++) { + if (escaped) { + data[start + valueLen] = data[i]; + valueLen += 1; + escaped = false; + } else { + if (data[i] == "\\") { + escaped = true; + } else if (data[i] == "'") { + return data.substring(start, valueLen); + } else { + data[start + valueLen] = data[i]; + valueLen += 1; + } + } + } + } else if (state == STATE_UNQUOTED_VALUE) { + uint256 start = i; + for (; i < len; i++) { + if (data[i] == " ") { + return data.substring(start, i - start); + } + } + return data.substring(start, len - start); + } else if (state == STATE_IGNORED_VALUE) { + if (data[i] == "'") { + state = STATE_IGNORED_QUOTED_VALUE; + i += 1; + } else { + state = STATE_IGNORED_UNQUOTED_VALUE; + } + } else if (state == STATE_IGNORED_QUOTED_VALUE) { + bool escaped = false; + for (; i < len; i++) { + if (escaped) { + escaped = false; + } else { + if (data[i] == "\\") { + escaped = true; + } else if (data[i] == "'") { + i += 1; + if (data[i] == " ") { + i += 1; + } + state = STATE_START; + break; + } + } + } + } else { + assert(state == STATE_IGNORED_UNQUOTED_VALUE); + for (; i < len; i++) { + if (data[i] == " ") { + state = STATE_START; + i += 1; + break; + } + } + } + } + return ""; } } diff --git a/test/dnsregistrar/TestOffchainDNSResolver.js b/test/dnsregistrar/TestOffchainDNSResolver.js index 53cbe5d6..ef979d62 100644 --- a/test/dnsregistrar/TestOffchainDNSResolver.js +++ b/test/dnsregistrar/TestOffchainDNSResolver.js @@ -311,12 +311,10 @@ contract('OffchainDNSResolver', function (accounts) { ).encodeABI() const result = await doDNSResolveCallback( name, - [`ENS1 ${resolver.address} ${testAddress}`], + [`ENS1 ${resolver.address} a[60]=${testAddress}`], callData, ) - expect( - ethers.utils.defaultAbiCoder.decode(['address'], result)[0], - ).to.equal(testAddress) + expect(result).to.equal(testAddress.toLowerCase()) }) it('correctly handles extra data in the TXT record when calling a resolver that supports address resolution with valid cointype', async function () { @@ -331,12 +329,10 @@ contract('OffchainDNSResolver', function (accounts) { ).encodeABI() const result = await doDNSResolveCallback( name, - [`ENS1 ${resolver.address} ${testAddress}`], + [`ENS1 ${resolver.address} a[${COIN_TYPE_ETH}]=${testAddress}`], callData, ) - expect( - ethers.utils.defaultAbiCoder.decode(['address'], result)[0], - ).to.equal(testAddress) + expect(result).to.equal(testAddress.toLowerCase()) }) it('handles extra data in the TXT record when calling a resolver that supports address resolution with invalid cointype', async function () { @@ -351,12 +347,10 @@ contract('OffchainDNSResolver', function (accounts) { ).encodeABI() const result = await doDNSResolveCallback( name, - [`ENS1 ${resolver.address} ${testAddress}`], + [`ENS1 ${resolver.address} a[60]=${testAddress}`], callData, ) - expect(ethers.utils.defaultAbiCoder.decode(['string'], result)[0]).to.equal( - '', - ) + expect(result).to.equal(null) }) it('raise an error if extra (address) data in the TXT record is invalid', async function () { @@ -370,7 +364,7 @@ contract('OffchainDNSResolver', function (accounts) { await expect( doDNSResolveCallback( name, - [`ENS1 ${resolver.address} ${testAddress}`], + [`ENS1 ${resolver.address} a[60]=${testAddress}`], callData, ), ).to.be.revertedWith('InvalidAddressFormat') diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js new file mode 100644 index 00000000..955ec377 --- /dev/null +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -0,0 +1,81 @@ +const ExtendedDNSResolver = artifacts.require('ExtendedDNSResolver.sol') +const namehash = require('eth-ens-namehash') +const { expect } = require('chai') +const packet = require('dns-packet') + +function hexEncodeName(name) { + return '0x' + packet.name.encode(name).toString('hex') +} + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +contract('ExtendedDNSResolver', function (accounts) { + var resolver = null + var PublicResolver = null + + beforeEach(async function () { + resolver = await ExtendedDNSResolver.new() + PublicResolver = await ethers.getContractFactory('PublicResolver') + }) + + async function resolve(name, method, args, context) { + const node = namehash.hash(name) + const callData = PublicResolver.interface.encodeFunctionData(method, [ + node, + ...args, + ]) + return resolver.resolve( + hexEncodeName(name), + callData, + ethers.utils.hexlify(ethers.utils.toUtf8Bytes(context)), + ) + } + + describe('a records', async () => { + it('resolves Ethereum addresses using addr(bytes32)', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32)', + [], + `a[60]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) + + it('resolves Ethereum addresses using addr(bytes32,uint256)', async function () { + const COIN_TYPE_ETH = 60 + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32,uint256)', + [COIN_TYPE_ETH], + `a[60]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) + + it('ignores records with the wrong cointype', async function () { + const COIN_TYPE_BTC = 0 + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32,uint256)', + [COIN_TYPE_BTC], + `a[60]=${testAddress}`, + ) + expect(result).to.equal(null) + }) + + it('raise an error for invalid hex data', async function () { + const name = 'test.test' + const testAddress = '0xfoobar' + await expect( + resolve(name, 'addr(bytes32)', [], `a[60]=${testAddress}`), + ).to.be.revertedWith('InvalidAddressFormat') + }) + }) +}) From c321f914c8a2a15baa162639b841d72bed288b64 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 20 Feb 2024 09:47:28 +0000 Subject: [PATCH 03/11] More tests --- test/resolvers/TestExtendedDNSResolver.js | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js index 955ec377..db076cc6 100644 --- a/test/resolvers/TestExtendedDNSResolver.js +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -77,5 +77,41 @@ contract('ExtendedDNSResolver', function (accounts) { resolve(name, 'addr(bytes32)', [], `a[60]=${testAddress}`), ).to.be.revertedWith('InvalidAddressFormat') }) + + it('works if the record comes after an unrelated one', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32)', + [], + `foo=bar a[60]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) + + it('works if the record comes after one for another cointype', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32)', + [], + `a[0]=0x1234 a[60]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) + + it('uses the first matching record it finds', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32)', + [], + `a[60]=${testAddress} a[60]=0x1234567890123456789012345678901234567890`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) }) }) From 4472f47587d7ff0c6e8678c8216f906761e0566d Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 20 Feb 2024 10:19:30 +0000 Subject: [PATCH 04/11] Text record support --- .../profiles/ExtendedDNSResolver.sol | 52 ++++++++++++++++- test/resolvers/TestExtendedDNSResolver.js | 58 ++++++++++++++++++- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index c6fd467a..c100ac87 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -6,9 +6,44 @@ import "@openzeppelin/contracts/utils/Strings.sol"; import "../../resolvers/profiles/IExtendedDNSResolver.sol"; import "../../resolvers/profiles/IAddressResolver.sol"; import "../../resolvers/profiles/IAddrResolver.sol"; +import "../../resolvers/profiles/ITextResolver.sol"; import "../../utils/HexUtils.sol"; import "../../dnssec-oracle/BytesUtils.sol"; +/** + * @dev Resolves names on ENS by interpreting record data stored in a DNS TXT record. + * This resolver implements the IExtendedDNSResolver interface, meaning that when + * a DNS name specifies it as the resolver via a TXT record, this resolver's + * resolve() method is invoked, and is passed any additional information from that + * text record. This resolver implements a simple text parser allowing a variety + * of records to be specified in text, which will then be used to resolve the name + * in ENS. + * + * To use this, set a TXT record on your DNS name in the following format: + * ENS1
+ * + * For example: + * ENS1 2.dnsname.ens.eth a[60]=0x1234... + * + * The record data consists of a series of key=value pairs, separated by spaces. Keys + * may have an optional argument in square brackets, and values may be either unquoted + * - in which case they may not contain spaces - or single-quoted. Single quotes in + * a quoted value may be backslash-escaped. + * + * Record types: + * - a[coinId] - Specifies how an `addr()` request should be resolved for the specified + * coinId. Ethereum has coinId 60. The value must be 0x-prefixed hexadecimal, and will + * be returned unmodified; this means that non-EVM addresses will need to be translated + * into binary format and then encoded in hex. + * Examples: + * - a[60]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7 + * - a[0]=0x00149010587f8364b964fcaa70687216b53bd2cbd798 + * - t[key] - Specifies how a `text()` request should be resolved for the specified key. + * Examples: + * - t[com.twitter]=nicksdjohnson + * - t[url]='https://ens.domains/' + * - t[note]='I\'m great' + */ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { using HexUtils for *; using BytesUtils for *; @@ -32,9 +67,11 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { ) external pure override returns (bytes memory) { bytes4 selector = bytes4(data); if (selector == IAddrResolver.addr.selector) { - return _resolveAddr(data, context); + return _resolveAddr(context); } else if (selector == IAddressResolver.addr.selector) { return _resolveAddress(data, context); + } else if (selector == ITextResolver.text.selector) { + return _resolveText(data, context); } revert NotImplemented(); } @@ -57,7 +94,6 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { } function _resolveAddr( - bytes calldata /* data */, bytes calldata context ) internal pure returns (bytes memory) { bytes memory value = _findValue(context, "a[60]="); @@ -69,6 +105,18 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { return record; } + function _resolveText( + bytes calldata data, + bytes calldata context + ) internal pure returns (bytes memory) { + (, string memory key) = abi.decode(data[4:], (bytes32, string)); + bytes memory value = _findValue( + context, + bytes.concat("t[", bytes(key), "]=") + ); + return value; + } + uint256 constant STATE_START = 0; uint256 constant STATE_IGNORED_KEY = 1; uint256 constant STATE_IGNORED_KEY_ARG = 2; diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js index db076cc6..5ed71f0a 100644 --- a/test/resolvers/TestExtendedDNSResolver.js +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -24,11 +24,13 @@ contract('ExtendedDNSResolver', function (accounts) { node, ...args, ]) - return resolver.resolve( + const resolveArgs = [ hexEncodeName(name), callData, ethers.utils.hexlify(ethers.utils.toUtf8Bytes(context)), - ) + ] + // console.log(resolveArgs); + return resolver.resolve(...resolveArgs) } describe('a records', async () => { @@ -114,4 +116,56 @@ contract('ExtendedDNSResolver', function (accounts) { expect(result).to.equal(testAddress.toLowerCase()) }) }) + + describe('t records', async () => { + it('decodes an unquoted t record', async function () { + const name = 'test.test' + const result = await resolve( + name, + 'text', + ['com.twitter'], + 't[com.twitter]=nicksdjohnson', + ) + expect(ethers.utils.toUtf8String(ethers.utils.arrayify(result))).to.equal( + 'nicksdjohnson', + ) + }) + + it('returns null for a missing key', async function () { + const name = 'test.test' + const result = await resolve( + name, + 'text', + ['com.discord'], + 't[com.twitter]=nicksdjohnson', + ) + expect(result).to.equal(null) + }) + + it('decodes a quoted t record', async function () { + const name = 'test.test' + const result = await resolve( + name, + 'text', + ['url'], + "t[url]='https://ens.domains/'", + ) + expect(ethers.utils.toUtf8String(ethers.utils.arrayify(result))).to.equal( + 'https://ens.domains/', + ) + }) + + it('handles escaped quotes', async function () { + const name = 'test.test' + const result = await resolve( + name, + 'text', + ['note'], + "t[note]='I\\'m great'", + ) + expect(ethers.utils.toUtf8String(ethers.utils.arrayify(result))).to.equal( + "I'm great", + ) + }) + }) }) From 23a3d666d8edfb877d4e9797e4d43cc84501b8f3 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 20 Feb 2024 10:22:31 +0000 Subject: [PATCH 05/11] Remove console log --- test/resolvers/TestExtendedDNSResolver.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js index 5ed71f0a..12ec8e54 100644 --- a/test/resolvers/TestExtendedDNSResolver.js +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -29,7 +29,6 @@ contract('ExtendedDNSResolver', function (accounts) { callData, ethers.utils.hexlify(ethers.utils.toUtf8Bytes(context)), ] - // console.log(resolveArgs); return resolver.resolve(...resolveArgs) } From 513fbe124a43af9200f662f437ea061f1d181cd2 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 21 Feb 2024 11:19:51 +0000 Subject: [PATCH 06/11] Add support for easier encoding of EVM chain IDs --- .../profiles/ExtendedDNSResolver.sol | 34 +++++++++++++++---- test/resolvers/TestExtendedDNSResolver.js | 14 ++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index c100ac87..03867b46 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -31,14 +31,22 @@ import "../../dnssec-oracle/BytesUtils.sol"; * a quoted value may be backslash-escaped. * * Record types: - * - a[coinId] - Specifies how an `addr()` request should be resolved for the specified - * coinId. Ethereum has coinId 60. The value must be 0x-prefixed hexadecimal, and will + * - a[] - Specifies how an `addr()` request should be resolved for the specified + * `coinType`. Ethereum has `coinType` 60. The value must be 0x-prefixed hexadecimal, and will * be returned unmodified; this means that non-EVM addresses will need to be translated * into binary format and then encoded in hex. * Examples: * - a[60]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7 * - a[0]=0x00149010587f8364b964fcaa70687216b53bd2cbd798 - * - t[key] - Specifies how a `text()` request should be resolved for the specified key. + * - a[e] - Specifies how an `addr()` request should be resolved for the specified + * `chainId`. The value must be 0x-prefixed hexadecimal. When encoding an address for an + * EVM-based cryptocurrency that uses a chainId instead of a coinType, this syntax *must* + * be used in place of the coin type - eg, Optimism is `a[e10]`, not `a[2147483658]`. + * A list of supported cryptocurrencies for both syntaxes can be found here: + * https://github.com/ensdomains/address-encoder/blob/master/docs/supported-cryptocurrencies.md + * Example: + * - a[e10]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7 + * - t[] - Specifies how a `text()` request should be resolved for the specified `key`. * Examples: * - t[com.twitter]=nicksdjohnson * - t[url]='https://ens.domains/' @@ -81,10 +89,22 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { bytes calldata context ) internal pure returns (bytes memory) { (, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256)); - bytes memory value = _findValue( - context, - bytes.concat("a[", bytes(coinType.toString()), "]=") - ); + bytes memory value; + if (coinType & 0x80000000 != 0) { + value = _findValue( + context, + bytes.concat( + "a[e", + bytes((coinType & 0x7fffffff).toString()), + "]=" + ) + ); + } else { + value = _findValue( + context, + bytes.concat("a[", bytes(coinType.toString()), "]=") + ); + } if (value.length == 0) { return value; } diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js index 12ec8e54..ccd28141 100644 --- a/test/resolvers/TestExtendedDNSResolver.js +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -114,6 +114,20 @@ contract('ExtendedDNSResolver', function (accounts) { ) expect(result).to.equal(testAddress.toLowerCase()) }) + + it('resolves addresses with coin types', async function () { + const CHAIN_ID_OPTIMISM = 10 + const COIN_TYPE_OPTIMISM = (0x80000000 | CHAIN_ID_OPTIMISM) >>> 0 + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32,uint256)', + [COIN_TYPE_OPTIMISM], + `a[e${CHAIN_ID_OPTIMISM}]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) }) describe('t records', async () => { From c9535ae5dab8cbc7ab9a6d15826673a216acf3ff Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 21 Feb 2024 11:23:08 +0000 Subject: [PATCH 07/11] Handle multiple spaces between records --- .../profiles/ExtendedDNSResolver.sol | 6 +++-- test/resolvers/TestExtendedDNSResolver.js | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index 03867b46..7154c367 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -236,7 +236,7 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { escaped = true; } else if (data[i] == "'") { i += 1; - if (data[i] == " ") { + while (data[i] == " ") { i += 1; } state = STATE_START; @@ -248,8 +248,10 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { assert(state == STATE_IGNORED_UNQUOTED_VALUE); for (; i < len; i++) { if (data[i] == " ") { + while (data[i] == " ") { + i += 1; + } state = STATE_START; - i += 1; break; } } diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js index ccd28141..0640dfd1 100644 --- a/test/resolvers/TestExtendedDNSResolver.js +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -91,6 +91,30 @@ contract('ExtendedDNSResolver', function (accounts) { expect(result).to.equal(testAddress.toLowerCase()) }) + it('handles multiple spaces between records', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32)', + [], + `foo=bar a[60]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) + + it('handles multiple spaces between quoted records', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32)', + [], + `foo='bar' a[60]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) + it('works if the record comes after one for another cointype', async function () { const name = 'test.test' const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' From 6fc58e6a5c78522896c24a559b6f8d3bb2f48794 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 21 Feb 2024 11:23:39 +0000 Subject: [PATCH 08/11] Handle no spaces between quoted records --- test/resolvers/TestExtendedDNSResolver.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js index 0640dfd1..dad82b69 100644 --- a/test/resolvers/TestExtendedDNSResolver.js +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -115,6 +115,18 @@ contract('ExtendedDNSResolver', function (accounts) { expect(result).to.equal(testAddress.toLowerCase()) }) + it('handles no spaces between quoted records', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const result = await resolve( + name, + 'addr(bytes32)', + [], + `foo='bar'a[60]=${testAddress}`, + ) + expect(result).to.equal(testAddress.toLowerCase()) + }) + it('works if the record comes after one for another cointype', async function () { const name = 'test.test' const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' From 38407f06c4d8d26433786c1ee2f32e5ed580984d Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 21 Feb 2024 13:55:22 +0000 Subject: [PATCH 09/11] Add comment referencing ensip-11 --- contracts/resolvers/profiles/ExtendedDNSResolver.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index 7154c367..41344e23 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -90,6 +90,7 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { ) internal pure returns (bytes memory) { (, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256)); bytes memory value; + // Per https://docs.ens.domains/ensip/11#specification if (coinType & 0x80000000 != 0) { value = _findValue( context, From 8dd3f112a911d0d21c935448da974384017589fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Wed, 27 Mar 2024 00:49:00 +0100 Subject: [PATCH 10/11] unify utility contracts --- contracts/dnsregistrar/DNSClaimChecker.sol | 2 +- contracts/dnsregistrar/DNSRegistrar.sol | 2 +- .../dnsregistrar/OffchainDNSResolver.sol | 2 +- contracts/dnsregistrar/RecordParser.sol | 2 +- .../dnsregistrar/TLDPublicSuffixList.sol | 2 +- contracts/dnsregistrar/mocks/DummyParser.sol | 2 +- contracts/dnssec-oracle/DNSSECImpl.sol | 2 +- contracts/dnssec-oracle/RRUtils.sol | 2 +- .../algorithms/P256SHA256Algorithm.sol | 2 +- .../algorithms/RSASHA1Algorithm.sol | 2 +- .../algorithms/RSASHA256Algorithm.sol | 2 +- .../dnssec-oracle/algorithms/RSAVerify.sol | 2 +- .../dnssec-oracle/digests/SHA1Digest.sol | 2 +- .../dnssec-oracle/digests/SHA256Digest.sol | 2 +- .../ethregistrar/ETHRegistrarController.sol | 2 +- contracts/ethregistrar/StablePriceOracle.sol | 2 +- contracts/ethregistrar/StringUtils.sol | 32 ------- .../profiles/ExtendedDNSResolver.sol | 2 +- .../{dnssec-oracle => utils}/BytesUtils.sol | 40 +++++++++ contracts/utils/HexUtils.sol | 25 ++++++ contracts/utils/NameEncoder.sol | 2 +- contracts/utils/StringUtils.sol | 85 +++++++++++++++++++ .../test => utils}/TestBytesUtils.sol | 2 +- contracts/utils/UniversalResolver.sol | 2 +- contracts/wrapper/BytesUtils.sol | 62 -------------- contracts/wrapper/NameWrapper.sol | 2 +- contracts/wrapper/mocks/TestUnwrap.sol | 2 +- .../wrapper/mocks/UpgradedNameWrapperMock.sol | 2 +- contracts/wrapper/test/NameGriefer.sol | 2 +- test/dnssec-oracle/TestBytesUtils.sol | 2 +- test/dnssec-oracle/TestRRUtils.sol | 2 +- test/utils/TestStringUtils.js | 30 +++++++ test/utils/mocks/StringUtilsTest.sol | 10 +++ test/wrapper/BytesUtils.js | 2 +- 34 files changed, 217 insertions(+), 121 deletions(-) delete mode 100644 contracts/ethregistrar/StringUtils.sol rename contracts/{dnssec-oracle => utils}/BytesUtils.sol (89%) create mode 100644 contracts/utils/StringUtils.sol rename contracts/{wrapper/test => utils}/TestBytesUtils.sol (90%) delete mode 100644 contracts/wrapper/BytesUtils.sol create mode 100644 test/utils/TestStringUtils.js create mode 100644 test/utils/mocks/StringUtilsTest.sol diff --git a/contracts/dnsregistrar/DNSClaimChecker.sol b/contracts/dnsregistrar/DNSClaimChecker.sol index 54950d1c..5e70488d 100644 --- a/contracts/dnsregistrar/DNSClaimChecker.sol +++ b/contracts/dnsregistrar/DNSClaimChecker.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.4; import "../dnssec-oracle/DNSSEC.sol"; -import "../dnssec-oracle/BytesUtils.sol"; import "../dnssec-oracle/RRUtils.sol"; +import "../utils/BytesUtils.sol"; import "../utils/HexUtils.sol"; import "@ensdomains/buffer/contracts/Buffer.sol"; diff --git a/contracts/dnsregistrar/DNSRegistrar.sol b/contracts/dnsregistrar/DNSRegistrar.sol index 953a9a3e..2ac4cbfa 100644 --- a/contracts/dnsregistrar/DNSRegistrar.sol +++ b/contracts/dnsregistrar/DNSRegistrar.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.4; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@ensdomains/buffer/contracts/Buffer.sol"; -import "../dnssec-oracle/BytesUtils.sol"; import "../dnssec-oracle/DNSSEC.sol"; import "../dnssec-oracle/RRUtils.sol"; import "../registry/ENSRegistry.sol"; import "../root/Root.sol"; import "../resolvers/profiles/AddrResolver.sol"; +import "../utils/BytesUtils.sol"; import "./DNSClaimChecker.sol"; import "./PublicSuffixList.sol"; import "./IDNSRegistrar.sol"; diff --git a/contracts/dnsregistrar/OffchainDNSResolver.sol b/contracts/dnsregistrar/OffchainDNSResolver.sol index bab59889..a15e27b8 100644 --- a/contracts/dnsregistrar/OffchainDNSResolver.sol +++ b/contracts/dnsregistrar/OffchainDNSResolver.sol @@ -5,11 +5,11 @@ import "../../contracts/resolvers/profiles/IAddrResolver.sol"; import "../../contracts/resolvers/profiles/IExtendedResolver.sol"; import "../../contracts/resolvers/profiles/IExtendedDNSResolver.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "../dnssec-oracle/BytesUtils.sol"; import "../dnssec-oracle/DNSSEC.sol"; import "../dnssec-oracle/RRUtils.sol"; import "../registry/ENSRegistry.sol"; import "../utils/HexUtils.sol"; +import "../utils/BytesUtils.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {LowLevelCallUtils} from "../utils/LowLevelCallUtils.sol"; diff --git a/contracts/dnsregistrar/RecordParser.sol b/contracts/dnsregistrar/RecordParser.sol index 5d333682..6e940605 100644 --- a/contracts/dnsregistrar/RecordParser.sol +++ b/contracts/dnsregistrar/RecordParser.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.11; -import "../dnssec-oracle/BytesUtils.sol"; +import "../utils/BytesUtils.sol"; library RecordParser { using BytesUtils for bytes; diff --git a/contracts/dnsregistrar/TLDPublicSuffixList.sol b/contracts/dnsregistrar/TLDPublicSuffixList.sol index 86bcd341..8bf72db6 100644 --- a/contracts/dnsregistrar/TLDPublicSuffixList.sol +++ b/contracts/dnsregistrar/TLDPublicSuffixList.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.4; -import "../dnssec-oracle/BytesUtils.sol"; +import "../utils/BytesUtils.sol"; import "./PublicSuffixList.sol"; /** diff --git a/contracts/dnsregistrar/mocks/DummyParser.sol b/contracts/dnsregistrar/mocks/DummyParser.sol index dab09434..403d3dc8 100644 --- a/contracts/dnsregistrar/mocks/DummyParser.sol +++ b/contracts/dnsregistrar/mocks/DummyParser.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.4; -import "../../dnssec-oracle/BytesUtils.sol"; +import "../../utils/BytesUtils.sol"; import "../RecordParser.sol"; contract DummyParser { diff --git a/contracts/dnssec-oracle/DNSSECImpl.sol b/contracts/dnssec-oracle/DNSSECImpl.sol index a3e4e5f8..bf8ddeb2 100644 --- a/contracts/dnssec-oracle/DNSSECImpl.sol +++ b/contracts/dnssec-oracle/DNSSECImpl.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.4; pragma experimental ABIEncoderV2; import "./Owned.sol"; -import "./BytesUtils.sol"; import "./RRUtils.sol"; import "./DNSSEC.sol"; import "./algorithms/Algorithm.sol"; import "./digests/Digest.sol"; +import "../utils/BytesUtils.sol"; import "@ensdomains/buffer/contracts/Buffer.sol"; /* diff --git a/contracts/dnssec-oracle/RRUtils.sol b/contracts/dnssec-oracle/RRUtils.sol index 20fbf15f..6f356ecc 100644 --- a/contracts/dnssec-oracle/RRUtils.sol +++ b/contracts/dnssec-oracle/RRUtils.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.4; -import "./BytesUtils.sol"; +import "../utils/BytesUtils.sol"; import "@ensdomains/buffer/contracts/Buffer.sol"; /** diff --git a/contracts/dnssec-oracle/algorithms/P256SHA256Algorithm.sol b/contracts/dnssec-oracle/algorithms/P256SHA256Algorithm.sol index 22f72e70..ece3633d 100644 --- a/contracts/dnssec-oracle/algorithms/P256SHA256Algorithm.sol +++ b/contracts/dnssec-oracle/algorithms/P256SHA256Algorithm.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.4; import "./Algorithm.sol"; import "./EllipticCurve.sol"; -import "../BytesUtils.sol"; +import "../../utils/BytesUtils.sol"; contract P256SHA256Algorithm is Algorithm, EllipticCurve { using BytesUtils for *; diff --git a/contracts/dnssec-oracle/algorithms/RSASHA1Algorithm.sol b/contracts/dnssec-oracle/algorithms/RSASHA1Algorithm.sol index 574f3f3e..73770747 100644 --- a/contracts/dnssec-oracle/algorithms/RSASHA1Algorithm.sol +++ b/contracts/dnssec-oracle/algorithms/RSASHA1Algorithm.sol @@ -1,8 +1,8 @@ pragma solidity ^0.8.4; import "./Algorithm.sol"; -import "../BytesUtils.sol"; import "./RSAVerify.sol"; +import "../../utils/BytesUtils.sol"; import "@ensdomains/solsha1/contracts/SHA1.sol"; /** diff --git a/contracts/dnssec-oracle/algorithms/RSASHA256Algorithm.sol b/contracts/dnssec-oracle/algorithms/RSASHA256Algorithm.sol index 08573384..9d85c067 100644 --- a/contracts/dnssec-oracle/algorithms/RSASHA256Algorithm.sol +++ b/contracts/dnssec-oracle/algorithms/RSASHA256Algorithm.sol @@ -1,8 +1,8 @@ pragma solidity ^0.8.4; import "./Algorithm.sol"; -import "../BytesUtils.sol"; import "./RSAVerify.sol"; +import "../../utils/BytesUtils.sol"; /** * @dev Implements the DNSSEC RSASHA256 algorithm. diff --git a/contracts/dnssec-oracle/algorithms/RSAVerify.sol b/contracts/dnssec-oracle/algorithms/RSAVerify.sol index 4db05a31..9a1f0569 100644 --- a/contracts/dnssec-oracle/algorithms/RSAVerify.sol +++ b/contracts/dnssec-oracle/algorithms/RSAVerify.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.4; -import "../BytesUtils.sol"; import "./ModexpPrecompile.sol"; +import "../../utils/BytesUtils.sol"; library RSAVerify { /** diff --git a/contracts/dnssec-oracle/digests/SHA1Digest.sol b/contracts/dnssec-oracle/digests/SHA1Digest.sol index 97e1247b..33f6f5b7 100644 --- a/contracts/dnssec-oracle/digests/SHA1Digest.sol +++ b/contracts/dnssec-oracle/digests/SHA1Digest.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.4; import "./Digest.sol"; -import "../BytesUtils.sol"; +import "../../utils/BytesUtils.sol"; import "@ensdomains/solsha1/contracts/SHA1.sol"; /** diff --git a/contracts/dnssec-oracle/digests/SHA256Digest.sol b/contracts/dnssec-oracle/digests/SHA256Digest.sol index f19867e6..0bcc081a 100644 --- a/contracts/dnssec-oracle/digests/SHA256Digest.sol +++ b/contracts/dnssec-oracle/digests/SHA256Digest.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.4; import "./Digest.sol"; -import "../BytesUtils.sol"; +import "../../utils/BytesUtils.sol"; /** * @dev Implements the DNSSEC SHA256 digest. diff --git a/contracts/ethregistrar/ETHRegistrarController.sol b/contracts/ethregistrar/ETHRegistrarController.sol index fd240b5a..fa144fa7 100644 --- a/contracts/ethregistrar/ETHRegistrarController.sol +++ b/contracts/ethregistrar/ETHRegistrarController.sol @@ -2,7 +2,7 @@ pragma solidity ~0.8.17; import {BaseRegistrarImplementation} from "./BaseRegistrarImplementation.sol"; -import {StringUtils} from "./StringUtils.sol"; +import {StringUtils} from "../utils/StringUtils.sol"; import {Resolver} from "../resolvers/Resolver.sol"; import {ENS} from "../registry/ENS.sol"; import {ReverseRegistrar} from "../reverseRegistrar/ReverseRegistrar.sol"; diff --git a/contracts/ethregistrar/StablePriceOracle.sol b/contracts/ethregistrar/StablePriceOracle.sol index f5434914..c4b90564 100644 --- a/contracts/ethregistrar/StablePriceOracle.sol +++ b/contracts/ethregistrar/StablePriceOracle.sol @@ -2,7 +2,7 @@ pragma solidity ~0.8.17; import "./IPriceOracle.sol"; -import "./StringUtils.sol"; +import "../utils/StringUtils.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; diff --git a/contracts/ethregistrar/StringUtils.sol b/contracts/ethregistrar/StringUtils.sol deleted file mode 100644 index 4b440c62..00000000 --- a/contracts/ethregistrar/StringUtils.sol +++ /dev/null @@ -1,32 +0,0 @@ -pragma solidity >=0.8.4; - -library StringUtils { - /** - * @dev Returns the length of a given string - * - * @param s The string to measure the length of - * @return The length of the input string - */ - function strlen(string memory s) internal pure returns (uint256) { - uint256 len; - uint256 i = 0; - uint256 bytelength = bytes(s).length; - for (len = 0; i < bytelength; len++) { - bytes1 b = bytes(s)[i]; - if (b < 0x80) { - i += 1; - } else if (b < 0xE0) { - i += 2; - } else if (b < 0xF0) { - i += 3; - } else if (b < 0xF8) { - i += 4; - } else if (b < 0xFC) { - i += 5; - } else { - i += 6; - } - } - return len; - } -} diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index 41344e23..d09ba27b 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -8,7 +8,7 @@ import "../../resolvers/profiles/IAddressResolver.sol"; import "../../resolvers/profiles/IAddrResolver.sol"; import "../../resolvers/profiles/ITextResolver.sol"; import "../../utils/HexUtils.sol"; -import "../../dnssec-oracle/BytesUtils.sol"; +import "../../utils/BytesUtils.sol"; /** * @dev Resolves names on ENS by interpreting record data stored in a DNS TXT record. diff --git a/contracts/dnssec-oracle/BytesUtils.sol b/contracts/utils/BytesUtils.sol similarity index 89% rename from contracts/dnssec-oracle/BytesUtils.sol rename to contracts/utils/BytesUtils.sol index 96344ce5..b1a13ca3 100644 --- a/contracts/dnssec-oracle/BytesUtils.sol +++ b/contracts/utils/BytesUtils.sol @@ -21,6 +21,46 @@ library BytesUtils { } } + /** + * @dev Returns the ENS namehash of a DNS-encoded name. + * @param self The DNS-encoded name to hash. + * @param offset The offset at which to start hashing. + * @return The namehash of the name. + */ + function namehash( + bytes memory self, + uint256 offset + ) internal pure returns (bytes32) { + (bytes32 labelhash, uint256 newOffset) = readLabel(self, offset); + if (labelhash == bytes32(0)) { + require(offset == self.length - 1, "namehash: Junk at end of name"); + return bytes32(0); + } + return + keccak256(abi.encodePacked(namehash(self, newOffset), labelhash)); + } + + /** + * @dev Returns the keccak-256 hash of a DNS-encoded label, and the offset to the start of the next label. + * @param self The byte string to read a label from. + * @param idx The index to read a label at. + * @return labelhash The hash of the label at the specified index, or 0 if it is the last label. + * @return newIdx The index of the start of the next label. + */ + function readLabel( + bytes memory self, + uint256 idx + ) internal pure returns (bytes32 labelhash, uint256 newIdx) { + require(idx < self.length, "readLabel: Index out of bounds"); + uint256 len = uint256(uint8(self[idx])); + if (len > 0) { + labelhash = keccak(self, idx + 1, len); + } else { + labelhash = bytes32(0); + } + newIdx = idx + len + 1; + } + /* * @dev Returns a positive number if `other` comes lexicographically after * `self`, a negative number if it comes before, or zero if the diff --git a/contracts/utils/HexUtils.sol b/contracts/utils/HexUtils.sol index 35ad52d0..0f799276 100644 --- a/contracts/utils/HexUtils.sol +++ b/contracts/utils/HexUtils.sol @@ -96,4 +96,29 @@ library HexUtils { (bytes32 r, bool valid) = hexStringToBytes32(str, idx, lastIdx); return (address(uint160(uint256(r))), valid); } + + /** + * @dev Attempts to convert an address to a hex string + * @param addr The _addr to parse + */ + function addressToHex(address addr) internal pure returns (string memory) { + bytes memory hexString = new bytes(40); + for (uint i = 0; i < 20; i++) { + bytes1 byteValue = bytes1(uint8(uint160(addr) >> (8 * (19 - i)))); + bytes1 highNibble = bytes1(uint8(byteValue) / 16); + bytes1 lowNibble = bytes1( + uint8(byteValue) - 16 * uint8(highNibble) + ); + hexString[2 * i] = _nibbleToHexChar(highNibble); + hexString[2 * i + 1] = _nibbleToHexChar(lowNibble); + } + return string(hexString); + } + + function _nibbleToHexChar( + bytes1 nibble + ) internal pure returns (bytes1 hexChar) { + if (uint8(nibble) < 10) return bytes1(uint8(nibble) + 0x30); + else return bytes1(uint8(nibble) + 0x57); + } } diff --git a/contracts/utils/NameEncoder.sol b/contracts/utils/NameEncoder.sol index 4cde36f3..5139297d 100644 --- a/contracts/utils/NameEncoder.sol +++ b/contracts/utils/NameEncoder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {BytesUtils} from "../wrapper/BytesUtils.sol"; +import {BytesUtils} from "../utils/BytesUtils.sol"; library NameEncoder { using BytesUtils for bytes; diff --git a/contracts/utils/StringUtils.sol b/contracts/utils/StringUtils.sol new file mode 100644 index 00000000..2dde4c42 --- /dev/null +++ b/contracts/utils/StringUtils.sol @@ -0,0 +1,85 @@ +pragma solidity >=0.8.4; + +library StringUtils { + /** + * @dev Returns the length of a given string + * + * @param s The string to measure the length of + * @return The length of the input string + */ + function strlen(string memory s) internal pure returns (uint256) { + uint256 len; + uint256 i = 0; + uint256 bytelength = bytes(s).length; + for (len = 0; i < bytelength; len++) { + bytes1 b = bytes(s)[i]; + if (b < 0x80) { + i += 1; + } else if (b < 0xE0) { + i += 2; + } else if (b < 0xF0) { + i += 3; + } else if (b < 0xF8) { + i += 4; + } else if (b < 0xFC) { + i += 5; + } else { + i += 6; + } + } + return len; + } + + /** + * @dev Escapes special characters in a given string + * + * @param str The string to escape + * @return The escaped string + */ + function escape(string memory str) internal pure returns (string memory) { + bytes memory strBytes = bytes(str); + uint extraChars = 0; + + // count extra space needed for escaping + for (uint i = 0; i < strBytes.length; i++) { + if (_needsEscaping(strBytes[i])) { + extraChars++; + } + } + + // allocate buffer with the exact size needed + bytes memory buffer = new bytes(strBytes.length + extraChars); + uint index = 0; + + // escape characters + for (uint i = 0; i < strBytes.length; i++) { + if (_needsEscaping(strBytes[i])) { + buffer[index++] = "\\"; + buffer[index++] = _getEscapedChar(strBytes[i]); + } else { + buffer[index++] = strBytes[i]; + } + } + + return string(buffer); + } + + // determine if a character needs escaping + function _needsEscaping(bytes1 char) private pure returns (bool) { + return + char == '"' || + char == "/" || + char == "\\" || + char == "\n" || + char == "\r" || + char == "\t"; + } + + // get the escaped character + function _getEscapedChar(bytes1 char) private pure returns (bytes1) { + if (char == "\n") return "n"; + if (char == "\r") return "r"; + if (char == "\t") return "t"; + return char; + } +} diff --git a/contracts/wrapper/test/TestBytesUtils.sol b/contracts/utils/TestBytesUtils.sol similarity index 90% rename from contracts/wrapper/test/TestBytesUtils.sol rename to contracts/utils/TestBytesUtils.sol index db0a2e97..bda4ab74 100644 --- a/contracts/wrapper/test/TestBytesUtils.sol +++ b/contracts/utils/TestBytesUtils.sol @@ -1,7 +1,7 @@ //SPDX-License-Identifier: MIT pragma solidity ~0.8.17; -import {BytesUtils} from "../BytesUtils.sol"; +import {BytesUtils} from "./BytesUtils.sol"; contract TestBytesUtils { using BytesUtils for *; diff --git a/contracts/utils/UniversalResolver.sol b/contracts/utils/UniversalResolver.sol index 33e39870..575e11f0 100644 --- a/contracts/utils/UniversalResolver.sol +++ b/contracts/utils/UniversalResolver.sol @@ -8,8 +8,8 @@ import {LowLevelCallUtils} from "./LowLevelCallUtils.sol"; import {ENS} from "../registry/ENS.sol"; import {IExtendedResolver} from "../resolvers/profiles/IExtendedResolver.sol"; import {Resolver, INameResolver, IAddrResolver} from "../resolvers/Resolver.sol"; +import {BytesUtils} from "../utils/BytesUtils.sol"; import {NameEncoder} from "./NameEncoder.sol"; -import {BytesUtils} from "../wrapper/BytesUtils.sol"; import {HexUtils} from "./HexUtils.sol"; error OffchainLookup( diff --git a/contracts/wrapper/BytesUtils.sol b/contracts/wrapper/BytesUtils.sol deleted file mode 100644 index ae65d077..00000000 --- a/contracts/wrapper/BytesUtils.sol +++ /dev/null @@ -1,62 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ~0.8.17; - -library BytesUtils { - /* - * @dev Returns the keccak-256 hash of a byte range. - * @param self The byte string to hash. - * @param offset The position to start hashing at. - * @param len The number of bytes to hash. - * @return The hash of the byte range. - */ - function keccak( - bytes memory self, - uint256 offset, - uint256 len - ) internal pure returns (bytes32 ret) { - require(offset + len <= self.length); - assembly { - ret := keccak256(add(add(self, 32), offset), len) - } - } - - /** - * @dev Returns the ENS namehash of a DNS-encoded name. - * @param self The DNS-encoded name to hash. - * @param offset The offset at which to start hashing. - * @return The namehash of the name. - */ - function namehash( - bytes memory self, - uint256 offset - ) internal pure returns (bytes32) { - (bytes32 labelhash, uint256 newOffset) = readLabel(self, offset); - if (labelhash == bytes32(0)) { - require(offset == self.length - 1, "namehash: Junk at end of name"); - return bytes32(0); - } - return - keccak256(abi.encodePacked(namehash(self, newOffset), labelhash)); - } - - /** - * @dev Returns the keccak-256 hash of a DNS-encoded label, and the offset to the start of the next label. - * @param self The byte string to read a label from. - * @param idx The index to read a label at. - * @return labelhash The hash of the label at the specified index, or 0 if it is the last label. - * @return newIdx The index of the start of the next label. - */ - function readLabel( - bytes memory self, - uint256 idx - ) internal pure returns (bytes32 labelhash, uint256 newIdx) { - require(idx < self.length, "readLabel: Index out of bounds"); - uint256 len = uint256(uint8(self[idx])); - if (len > 0) { - labelhash = keccak(self, idx + 1, len); - } else { - labelhash = bytes32(0); - } - newIdx = idx + len + 1; - } -} diff --git a/contracts/wrapper/NameWrapper.sol b/contracts/wrapper/NameWrapper.sol index 11ed27eb..888e2027 100644 --- a/contracts/wrapper/NameWrapper.sol +++ b/contracts/wrapper/NameWrapper.sol @@ -13,7 +13,7 @@ import {IBaseRegistrar} from "../ethregistrar/IBaseRegistrar.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {BytesUtils} from "./BytesUtils.sol"; +import {BytesUtils} from "../utils/BytesUtils.sol"; import {ERC20Recoverable} from "../utils/ERC20Recoverable.sol"; error Unauthorised(bytes32 node, address addr); diff --git a/contracts/wrapper/mocks/TestUnwrap.sol b/contracts/wrapper/mocks/TestUnwrap.sol index ff098174..8c2fe602 100644 --- a/contracts/wrapper/mocks/TestUnwrap.sol +++ b/contracts/wrapper/mocks/TestUnwrap.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.4; import "../../registry/ENS.sol"; import "../../ethregistrar/IBaseRegistrar.sol"; +import {BytesUtils} from "../../utils/BytesUtils.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {BytesUtils} from "../BytesUtils.sol"; contract TestUnwrap is Ownable { using BytesUtils for bytes; diff --git a/contracts/wrapper/mocks/UpgradedNameWrapperMock.sol b/contracts/wrapper/mocks/UpgradedNameWrapperMock.sol index 6516eaa8..e04ab032 100644 --- a/contracts/wrapper/mocks/UpgradedNameWrapperMock.sol +++ b/contracts/wrapper/mocks/UpgradedNameWrapperMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.4; import {INameWrapperUpgrade} from "../INameWrapperUpgrade.sol"; import "../../registry/ENS.sol"; import "../../ethregistrar/IBaseRegistrar.sol"; -import {BytesUtils} from "../BytesUtils.sol"; +import {BytesUtils} from "../../utils/BytesUtils.sol"; contract UpgradedNameWrapperMock is INameWrapperUpgrade { using BytesUtils for bytes; diff --git a/contracts/wrapper/test/NameGriefer.sol b/contracts/wrapper/test/NameGriefer.sol index b876b45c..437ab6e8 100644 --- a/contracts/wrapper/test/NameGriefer.sol +++ b/contracts/wrapper/test/NameGriefer.sol @@ -1,9 +1,9 @@ //SPDX-License-Identifier: MIT pragma solidity ~0.8.17; -import {BytesUtils} from "../BytesUtils.sol"; import {INameWrapper} from "../INameWrapper.sol"; import {ENS} from "../../registry/ENS.sol"; +import {BytesUtils} from "../../utils/BytesUtils.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; contract NameGriefer is IERC1155Receiver { diff --git a/test/dnssec-oracle/TestBytesUtils.sol b/test/dnssec-oracle/TestBytesUtils.sol index df0610c8..cd8900ff 100644 --- a/test/dnssec-oracle/TestBytesUtils.sol +++ b/test/dnssec-oracle/TestBytesUtils.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.4; import "../../contracts/dnssec-oracle/RRUtils.sol"; -import "../../contracts/dnssec-oracle/BytesUtils.sol"; +import "../../contracts/utils/BytesUtils.sol"; contract TestBytesUtils { using BytesUtils for *; diff --git a/test/dnssec-oracle/TestRRUtils.sol b/test/dnssec-oracle/TestRRUtils.sol index eb7d7742..726e8158 100644 --- a/test/dnssec-oracle/TestRRUtils.sol +++ b/test/dnssec-oracle/TestRRUtils.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.4; import "../../contracts/dnssec-oracle/RRUtils.sol"; -import "../../contracts/dnssec-oracle/BytesUtils.sol"; +import "../../contracts/utils/BytesUtils.sol"; contract TestRRUtils { using BytesUtils for *; diff --git a/test/utils/TestStringUtils.js b/test/utils/TestStringUtils.js new file mode 100644 index 00000000..7a725015 --- /dev/null +++ b/test/utils/TestStringUtils.js @@ -0,0 +1,30 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') + +describe('StringUtils', () => { + let stringUtils + + before(async () => { + const StringUtils = await ethers.getContractFactory('StringUtilsTest') + stringUtils = await StringUtils.deploy() + await stringUtils.deployed() + }) + + it('should escape double quote correctly based JSON standard', async () => { + expect(await stringUtils.testEscape('My ENS is, "tanrikulu.eth"')).to.equal( + 'My ENS is, \\"tanrikulu.eth\\"', + ) + }) + + it('should escape backslash correctly based JSON standard', async () => { + expect(await stringUtils.testEscape('Path\\to\\file')).to.equal( + 'Path\\\\to\\\\file', + ) + }) + + it('should escape new line character correctly based JSON standard', async () => { + expect(await stringUtils.testEscape('Line 1\nLine 2')).to.equal( + 'Line 1\\nLine 2', + ) + }) +}) diff --git a/test/utils/mocks/StringUtilsTest.sol b/test/utils/mocks/StringUtilsTest.sol new file mode 100644 index 00000000..ffce35c1 --- /dev/null +++ b/test/utils/mocks/StringUtilsTest.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +import "../../../contracts/utils/StringUtils.sol"; + +library StringUtilsTest { + function testEscape( + string calldata testStr + ) public pure returns (string memory) { + return StringUtils.escape(testStr); + } +} diff --git a/test/wrapper/BytesUtils.js b/test/wrapper/BytesUtils.js index 0590cf6f..e5290dbc 100644 --- a/test/wrapper/BytesUtils.js +++ b/test/wrapper/BytesUtils.js @@ -18,7 +18,7 @@ describe('BytesUtils', () => { before(async () => { const BytesUtilsFactory = await ethers.getContractFactory( - 'contracts/wrapper/test/TestBytesUtils.sol:TestBytesUtils', + 'contracts/utils/TestBytesUtils.sol:TestBytesUtils', ) BytesUtils = await BytesUtilsFactory.deploy() }) From 63ca27ee5acd21e942ce3a128153e77690e570e3 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Thu, 4 Apr 2024 09:20:39 +0100 Subject: [PATCH 11/11] Changesx in response to revie --- .../profiles/ExtendedDNSResolver.sol | 26 +++++++++++++++++++ test/resolvers/TestExtendedDNSResolver.js | 11 ++++++++ test/utils/TestHexUtils.js | 3 ++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/contracts/resolvers/profiles/ExtendedDNSResolver.sol b/contracts/resolvers/profiles/ExtendedDNSResolver.sol index 41344e23..ae50eca2 100644 --- a/contracts/resolvers/profiles/ExtendedDNSResolver.sol +++ b/contracts/resolvers/profiles/ExtendedDNSResolver.sol @@ -30,6 +30,18 @@ import "../../dnssec-oracle/BytesUtils.sol"; * - in which case they may not contain spaces - or single-quoted. Single quotes in * a quoted value may be backslash-escaped. * + * + * ┌────────┐ + * │ ┌───┐ │ + * ┌──────────────────────────────┴─┤" "│◄─┴────────────────────────────────────────┐ + * │ └───┘ │ + * │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌────────────┐ ┌───┐ │ + * ^─┴─►│key├─┬─►│"["├───►│arg├───►│"]"├─┬─►│"="├─┬─►│"'"├───►│quoted_value├───►│"'"├─┼─$ + * └───┘ │ └───┘ └───┘ └───┘ │ └───┘ │ └───┘ └────────────┘ └───┘ │ + * └──────────────────────────┘ │ ┌──────────────┐ │ + * └─────────►│unquoted_value├─────────┘ + * └──────────────┘ + * * Record types: * - a[] - Specifies how an `addr()` request should be resolved for the specified * `coinType`. Ethereum has `coinType` 60. The value must be 0x-prefixed hexadecimal, and will @@ -148,14 +160,28 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 { uint256 constant STATE_IGNORED_QUOTED_VALUE = 7; uint256 constant STATE_IGNORED_UNQUOTED_VALUE = 8; + /** + * @dev Implements a DFA to parse the text record, looking for an entry + * matching `key`. + * @param data The text record to parse. + * @param key The exact key to search for. + * @return value The value if found, or an empty string if `key` does not exist. + */ function _findValue( bytes memory data, bytes memory key ) internal pure returns (bytes memory value) { + // Here we use a simple state machine to parse the text record. We + // process characters one at a time; each character can trigger a + // transition to a new state, or terminate the DFA and return a value. + // For states that expect to process a number of tokens, we use + // inner loops for efficiency reasons, to avoid the need to go + // through the outer loop and switch statement for every character. uint256 state = STATE_START; uint256 len = data.length; for (uint256 i = 0; i < len; ) { if (state == STATE_START) { + // Look for a matching key. if (data.equals(i, key, 0, key.length)) { i += key.length; state = STATE_VALUE; diff --git a/test/resolvers/TestExtendedDNSResolver.js b/test/resolvers/TestExtendedDNSResolver.js index dad82b69..0916c19d 100644 --- a/test/resolvers/TestExtendedDNSResolver.js +++ b/test/resolvers/TestExtendedDNSResolver.js @@ -216,5 +216,16 @@ contract('ExtendedDNSResolver', function (accounts) { "I'm great", ) }) + + it('rejects a record with an unterminated quoted string', async function () { + const name = 'test.test' + const result = await resolve( + name, + 'text', + ['note'], + "t[note]='I\\'m great", + ) + expect(result).to.equal(null) + }) }) }) diff --git a/test/utils/TestHexUtils.js b/test/utils/TestHexUtils.js index f45d31bf..f333183e 100644 --- a/test/utils/TestHexUtils.js +++ b/test/utils/TestHexUtils.js @@ -7,6 +7,7 @@ use(solidity) const NULL_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' describe('HexUtils', () => { let HexUtils @@ -139,7 +140,7 @@ describe('HexUtils', () => { 39, ) expect(valid).to.equal(false) - expect(address).to.equal('0x0000000000000000000000000000000000000000') + expect(address).to.equal(ZERO_ADDRESS) }) })