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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions contracts/resolvers/DotnsContentResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,14 @@ contract DotnsContentResolver is
/// @notice Protocol-level address registry for all DotNS contracts.
IDotnsProtocolRegistry public protocolRegistry;

/// @notice Block number of the last `setContenthash` per node; zero when never recorded.
/// @dev Added in 1.1.0, taking one slot from `__gap`. Hashes written by 1.0.0 have no
/// record here and read zero until their next write.
mapping(bytes32 node => uint64 blockNumber) private contenthashUpdatedAtBlocks;

/// @dev Reserved storage space to allow for layout changes in the future.
// forge-lint: disable-next-line(mixed-case-variable)
uint256[50] private __gap;
uint256[49] private __gap;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand All @@ -68,6 +73,7 @@ contract DotnsContentResolver is
function setContenthash(bytes32 node, bytes calldata hash) external override {
_requireNodeOwnerOrOperator(node);
contenthashes[node] = hash;
contenthashUpdatedAtBlocks[node] = uint64(block.number);
emit ContentHashUpdated(node, hash);
}

Expand All @@ -76,6 +82,16 @@ contract DotnsContentResolver is
return contenthashes[node];
}

/// @inheritdoc IDotnsContentResolver
function contenthashUpdatedAtBlock(bytes32 node)
external
view
override
returns (uint64 blockNumber)
{
return contenthashUpdatedAtBlocks[node];
}

/// @inheritdoc IDotnsContentResolver
function setText(bytes32 node, string calldata key, string calldata value) external override {
_requireNodeOwnerOrOperator(node);
Expand Down Expand Up @@ -137,7 +153,7 @@ contract DotnsContentResolver is
/// @notice Returns implementation version.
/// @return versionString Current version string.
function version() external pure virtual returns (string memory versionString) {
versionString = "1.0.0";
versionString = "1.1.0";
}

/// @inheritdoc ERC165Upgradeable
Expand Down
14 changes: 12 additions & 2 deletions contracts/resolvers/IDotnsContentResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ interface IDotnsContentResolver {
/// @notice Sets the content hash for a node.
/// @dev The caller must own the node in the DotNS registry or be an approved operator,
/// otherwise @custom:reverts NotAuthorised. Content hashes are opaque bytes (e.g. an
/// IPFS CID); the resolver stores them as-is and never interprets the payload. Emits
/// @custom:emits ContentHashUpdated on every successful write.
/// IPFS CID); the resolver stores them as-is and never interprets the payload. Every
/// successful write records the current block number for `contenthashUpdatedAtBlock`
/// and emits @custom:emits ContentHashUpdated.
/// @param node The node whose content hash is being set.
/// @param hash Opaque content hash bytes.
function setContenthash(bytes32 node, bytes calldata hash) external;
Expand All @@ -44,6 +45,15 @@ interface IDotnsContentResolver {
/// @return hash The stored content hash bytes, or empty if unset.
function contenthash(bytes32 node) external view returns (bytes memory hash);

/// @notice Returns the block in which a node's content hash was last written.
/// @dev Recorded by every successful `setContenthash`, so it answers "when did this hash
/// last change" without scanning `ContentHashUpdated` logs. Zero means no write has
/// been recorded: the node's hash was never set, or it was set before this field
/// existed and has not been rewritten since.
/// @param node The node to query.
/// @return blockNumber Block number of the last `setContenthash`, or zero if none recorded.
function contenthashUpdatedAtBlock(bytes32 node) external view returns (uint64 blockNumber);

/// @notice Sets a text record for a node.
/// @dev The caller must own the node in the DotNS registry or be an approved operator,
/// otherwise @custom:reverts NotAuthorised. Text records are arbitrary key/value strings
Expand Down
83 changes: 83 additions & 0 deletions test/unit/resolver/DotnsContentResolver.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,87 @@ contract DotnsContentResolverTests is BaseDotns {

assertEq(dotnsContentResolver.text(node, textKey), textValue);
}

function test_contenthash_updated_at_block_is_zero_when_unset() public {
bytes32 node = _register("unsetblock01", ed, IPopRules.PopStatus.NoStatus);

assertEq(dotnsContentResolver.contenthashUpdatedAtBlock(node), 0);
}

function test_set_contenthash_records_block_number() public {
address nameOwner = ed;

bytes32 node = _register("blockrecord01", nameOwner, IPopRules.PopStatus.NoStatus);

bytes memory contentHash =
hex"e30101701220aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

vm.roll(1_234);
vm.startPrank(nameOwner);
dotnsContentResolver.setContenthash(node, contentHash);
vm.stopPrank();

assertEq(dotnsContentResolver.contenthashUpdatedAtBlock(node), 1_234);
}

function test_set_contenthash_again_moves_block_number() public {
address nameOwner = ed;

bytes32 node = _register("blockmove0001", nameOwner, IPopRules.PopStatus.NoStatus);

bytes memory contentHash =
hex"e30101701220aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

vm.roll(100);
vm.startPrank(nameOwner);
dotnsContentResolver.setContenthash(node, contentHash);
vm.stopPrank();
assertEq(dotnsContentResolver.contenthashUpdatedAtBlock(node), 100);

// A rewrite of the identical hash is still a write and is recorded as one, matching
// ContentHashUpdated, which is emitted on every call rather than only on a change.
vm.roll(250);
vm.startPrank(nameOwner);
dotnsContentResolver.setContenthash(node, contentHash);
vm.stopPrank();
assertEq(dotnsContentResolver.contenthashUpdatedAtBlock(node), 250);
}

function test_set_text_does_not_touch_contenthash_block() public {
address nameOwner = ed;

bytes32 node = _register("textnoblock01", nameOwner, IPopRules.PopStatus.NoStatus);

bytes memory contentHash =
hex"e30101701220aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

vm.roll(40);
vm.startPrank(nameOwner);
dotnsContentResolver.setContenthash(node, contentHash);
vm.stopPrank();

vm.roll(80);
vm.startPrank(nameOwner);
dotnsContentResolver.setText(node, "url", "https://example.org");
vm.stopPrank();

assertEq(dotnsContentResolver.contenthashUpdatedAtBlock(node), 40);
}

function testFuzz_set_contenthash_records_any_block(uint64 blockNumber) public {
blockNumber = uint64(bound(blockNumber, 1, type(uint64).max));
address nameOwner = ed;

bytes32 node = _register("fuzzblock0001", nameOwner, IPopRules.PopStatus.NoStatus);

bytes memory contentHash =
hex"e30101701220aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

vm.roll(blockNumber);
vm.startPrank(nameOwner);
dotnsContentResolver.setContenthash(node, contentHash);
vm.stopPrank();

assertEq(dotnsContentResolver.contenthashUpdatedAtBlock(node), blockNumber);
}
}
Loading