Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/dnssec updates #272

Merged
merged 18 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ yarn pub
6. Have the tagged commit audited if necessary
7. If changes are required, make the changes and then once ready for review create another GitHub release with an incremented RC value `v1.2.3-RC0` -> `v.1.2.3-RC1`. Repeat as necessary.
8. Deploy to testnet. Commit build artifacts to `feature` branch. You now MUST merge this branch into `staging` branch.
9. Create GitHub release of the form `v1.2.3-testnet` from the commit that has the new deployment artifacts.
9. Create GitHub release of the form `v1.2.3-testnet` from the commit that has the new deployment artifacts.
10. If any further changes are needed, you can either make them on the existing feature branch that is in sync or create a new branch, and follow steps 1 -> 9. Repeat as necessary.
11. Make Deployment to mainnet from `staging`. Commit build artifacts. You now MUST merge this branch into `main`.
12. Create GitHub release of the form `v1.2.3-mainnet` from the commit that has the new deployment artifacts.
Expand All @@ -192,11 +192,11 @@ yarn pub

- `staging` branch and `main` branch should start off in sync
- `staging` is intended to be a practice `main`. Only code that is intended to be released to `main` can be merged to `staging`. Consequently:
- Feature branches will be long-lived
- Feature branches must be kept in sync with `staging`
- Audits are conducted on feature branches
- Feature branches will be long-lived
- Feature branches must be kept in sync with `staging`
- Audits are conducted on feature branches
- All code that is on `staging` and `main` should be deployed to testnet and mainnet respectively i.e. these branches should not have any undeployed code
- It is preferable to not edit the same file on different feature branches.
- Code on `staging` and `main` will always be a subset of what is deployed, as smart contracts cannot be undeployed.
- Release candidates, `staging` and `main` branch are subject to our bug bounty
- Releases follow semantic versioning and should contain a description of changes with developers being the intended audience
- Releases follow semantic versioning and should contain a description of changes with developers being the intended audience
108 changes: 95 additions & 13 deletions contracts/dnsregistrar/OffchainDNSResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import "../dnssec-oracle/RRUtils.sol";
import "../registry/ENSRegistry.sol";
import "../utils/HexUtils.sol";

import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {LowLevelCallUtils} from "../utils/LowLevelCallUtils.sol";

error InvalidOperation();
error OffchainLookup(
address sender,
string[] urls,
Expand All @@ -31,6 +35,7 @@ uint16 constant TYPE_TXT = 16;

contract OffchainDNSResolver is IExtendedResolver {
using RRUtils for *;
using Address for address;
using BytesUtils for bytes;
using HexUtils for bytes;

Expand All @@ -50,16 +55,13 @@ contract OffchainDNSResolver is IExtendedResolver {
bytes calldata name,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = gatewayURL;

revert OffchainLookup(
address(this),
urls,
abi.encodeCall(IDNSGateway.resolve, (name, TYPE_TXT)),
OffchainDNSResolver.resolveCallback.selector,
abi.encode(name, data)
);
return
callWithOffchainLookupPropagation(
msg.sender,
name,
data,
abi.encodeCall(IExtendedResolver.resolve, (name, data))
);
}

function resolveCallback(
Expand Down Expand Up @@ -106,17 +108,30 @@ contract OffchainDNSResolver is IExtendedResolver {
)
) {
return
IExtendedDNSResolver(dnsresolver).resolve(
callWithOffchainLookupPropagation(
dnsresolver,
name,
query,
context
abi.encodeCall(
IExtendedDNSResolver.resolve,
(name, query, context)
)
);
} else if (
IERC165(dnsresolver).supportsInterface(
IExtendedResolver.resolve.selector
)
) {
return IExtendedResolver(dnsresolver).resolve(name, query);
return
callWithOffchainLookupPropagation(
dnsresolver,
name,
query,
abi.encodeCall(
IExtendedResolver.resolve,
(name, query)
)
);
} else {
(bool ok, bytes memory ret) = address(dnsresolver)
.staticcall(query);
Expand Down Expand Up @@ -223,4 +238,71 @@ contract OffchainDNSResolver is IExtendedResolver {
abi.encodePacked(parentNode, name.keccak(idx, separator - idx))
);
}

function callWithOffchainLookupPropagation(
address target,
bytes memory name,
bytes memory innerdata,
bytes memory data
) internal view returns (bytes memory) {
if (target.isContract()) {
bool result = LowLevelCallUtils.functionStaticCall(
address(target),
data
);
uint256 size = LowLevelCallUtils.returnDataSize();
if (result) {
bytes memory returnData = LowLevelCallUtils.readReturnData(
0,
size
);
return abi.decode(returnData, (bytes));
}
// Failure
if (size >= 4) {
bytes memory errorId = LowLevelCallUtils.readReturnData(0, 4);
if (bytes4(errorId) == OffchainLookup.selector) {
// Offchain lookup. Decode the revert message and create our own that nests it.
bytes memory revertData = LowLevelCallUtils.readReturnData(
4,
size - 4
);
(
address sender,
string[] memory urls,
bytes memory callData,
bytes4 innerCallbackFunction,
bytes memory extraData
) = abi.decode(
revertData,
(address, string[], bytes, bytes4, bytes)
);

if (sender != target) {
revert InvalidOperation();
}

revert OffchainLookup(
address(this),
urls,
callData,
OffchainDNSResolver.resolveCallback.selector,
abi.encode(sender, innerCallbackFunction, extraData)
);
}
}
LowLevelCallUtils.propagateRevert();
} else {
string[] memory urls = new string[](1);
urls[0] = gatewayURL;

revert OffchainLookup(
address(this),
urls,
abi.encodeCall(IDNSGateway.resolve, (name, TYPE_TXT)),
OffchainDNSResolver.resolveCallback.selector,
abi.encode(name, innerdata)
);
}
}
}
6 changes: 4 additions & 2 deletions contracts/dnsregistrar/RecordParser.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ library RecordParser {
" "
);
if (terminator == type(uint256).max) {
terminator = input.length;
terminator = len + offset;
nextOffset = terminator;
} else {
nextOffset = terminator + 1;
}

key = input.substring(offset, separator - offset);
value = input.substring(separator + 1, terminator - separator - 1);
nextOffset = terminator + 1;
}
}
37 changes: 37 additions & 0 deletions contracts/dnsregistrar/mocks/DummyNonCCIPAwareResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../OffchainDNSResolver.sol";
import "../../resolvers/profiles/IExtendedResolver.sol";

contract DummyNonCCIPAwareResolver is IExtendedResolver, ERC165 {
OffchainDNSResolver dnsResolver;

constructor(OffchainDNSResolver _dnsResolver) {
dnsResolver = _dnsResolver;
}

function supportsInterface(
bytes4 interfaceId
) public view virtual override returns (bool) {
return
interfaceId == type(IExtendedResolver).interfaceId ||
super.supportsInterface(interfaceId);
}

function resolve(
bytes calldata /* name */,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = "https://example.com/";
revert OffchainLookup(
address(dnsResolver),
urls,
data,
OffchainDNSResolver.resolveCallback.selector,
data
);
}
}
47 changes: 47 additions & 0 deletions contracts/dnsregistrar/mocks/DummyParser.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pragma solidity ^0.8.4;

import "../../dnssec-oracle/BytesUtils.sol";
import "../RecordParser.sol";

contract DummyParser {
using BytesUtils for bytes;

// parse data in format: name;key1=value1 key2=value2;url
function parseData(
bytes memory data,
uint256 kvCount
)
external
pure
returns (
string memory name,
string[] memory keys,
string[] memory values,
string memory url
)
{
uint256 len = data.length;
// retrieve name
uint256 sep1 = data.find(0, len, ";");
name = string(data.substring(0, sep1));

// retrieve url
uint256 sep2 = data.find(sep1 + 1, len - sep1, ";");
url = string(data.substring(sep2 + 1, len - sep2 - 1));

keys = new string[](kvCount);
values = new string[](kvCount);
// retrieve keys and values
uint256 offset = sep1 + 1;
for (uint256 i; i < kvCount && offset < len; i++) {
(
bytes memory key,
bytes memory val,
uint256 nextOffset
) = RecordParser.readKeyValue(data, offset, sep2 - offset);
keys[i] = string(key);
values[i] = string(val);
offset = nextOffset;
}
}
}
47 changes: 47 additions & 0 deletions contracts/resolvers/profiles/ExtendedDNSResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "../../resolvers/profiles/IExtendedDNSResolver.sol";
import "../../resolvers/profiles/IAddressResolver.sol";
import "../../resolvers/profiles/IAddrResolver.sol";
import "../../utils/HexUtils.sol";

contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 {
using HexUtils for *;

uint256 private constant COIN_TYPE_ETH = 60;

error NotImplemented();
error InvalidAddressFormat();

function supportsInterface(
bytes4 interfaceId
) external view virtual override returns (bool) {
return interfaceId == type(IExtendedDNSResolver).interfaceId;
}

function resolve(
bytes calldata /* name */,
bytes calldata data,
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);
}
revert NotImplemented();
}
}
4 changes: 4 additions & 0 deletions contracts/utils/HexUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ library HexUtils {
uint256 idx,
uint256 lastIdx
) internal pure returns (bytes32 r, bool valid) {
uint256 hexLength = lastIdx - idx;
if ((hexLength != 64 && hexLength != 40) || hexLength % 2 == 1) {
revert("Invalid string length");
}
valid = true;
assembly {
// check that the index to read to is not past the end of the string
Expand Down
20 changes: 20 additions & 0 deletions deploy/resolvers/00_deploy_extended_dns_resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ethers } from 'hardhat'
import { DeployFunction } from 'hardhat-deploy/types'
import { HardhatRuntimeEnvironment } from 'hardhat/types'

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { getNamedAccounts, deployments } = hre
const { deploy } = deployments
const { deployer } = await getNamedAccounts()

await deploy('ExtendedDNSResolver', {
from: deployer,
args: [],
log: true,
})
}

func.id = 'extended-dns-resolver'
func.tags = ['resolvers', 'ExtendedDNSResolver']

export default func
30 changes: 30 additions & 0 deletions test/dnsregistrar/Helpers/DummyParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { expect } = require('chai')
const { ethers } = require('hardhat')
const { toUtf8Bytes } = require('ethers/lib/utils')

describe('DummyParser', () => {
let parser

before(async () => {
const factory = await ethers.getContractFactory('DummyParser')
parser = await factory.deploy()
})

it('parse data', async () => {
const data = 'usdt;issuer=tether decimals=18;https://tether.to'
const [name, keys, values, url] = await parser.parseData(
toUtf8Bytes(data),
2,
)
// correct name
expect(name).to.eq('usdt')
// correct keys and values
expect(keys[0]).to.eq('issuer')
expect(values[0]).to.eq('tether')
expect(keys[1]).to.eq('decimals')
// incorrect last value
expect(values[1]).not.to.eq('18;https://tether.to')
// correct url
expect(url).to.eq('https://tether.to')
})
})
Loading