Skip to content

Commit b6697a0

Browse files
authored
feat: add ETHRegistrar and StandardRentPriceOracle (#6)
Add registrar contracts for ENS name registration: - ETHRegistrar: handles name registration and renewal with commit-reveal scheme - StandardRentPriceOracle: pricing logic with length-based rates, discounts, and premium pricing - LibHalving: utility for computing halving-based price decay
1 parent 096e967 commit b6697a0

File tree

9 files changed

+2267
-0
lines changed

9 files changed

+2267
-0
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >=0.8.13;
3+
4+
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
5+
6+
import {EnhancedAccessControl} from "../access-control/EnhancedAccessControl.sol";
7+
import {EACBaseRolesLib} from "../access-control/libraries/EACBaseRolesLib.sol";
8+
import {HCAEquivalence} from "../hca/HCAEquivalence.sol";
9+
import {IHCAFactoryBasic} from "../hca/interfaces/IHCAFactoryBasic.sol";
10+
import {IPermissionedRegistry} from "../registry/interfaces/IPermissionedRegistry.sol";
11+
import {IRegistry} from "../registry/interfaces/IRegistry.sol";
12+
import {IRegistryDatastore} from "../registry/interfaces/IRegistryDatastore.sol";
13+
import {RegistryRolesLib} from "../registry/libraries/RegistryRolesLib.sol";
14+
15+
import {IETHRegistrar} from "./interfaces/IETHRegistrar.sol";
16+
import {IRentPriceOracle} from "./interfaces/IRentPriceOracle.sol";
17+
18+
uint256 constant REGISTRATION_ROLE_BITMAP = 0 |
19+
RegistryRolesLib.ROLE_SET_SUBREGISTRY |
20+
RegistryRolesLib.ROLE_SET_SUBREGISTRY_ADMIN |
21+
RegistryRolesLib.ROLE_SET_RESOLVER |
22+
RegistryRolesLib.ROLE_SET_RESOLVER_ADMIN |
23+
RegistryRolesLib.ROLE_CAN_TRANSFER_ADMIN;
24+
25+
uint256 constant ROLE_SET_ORACLE = 1 << 0;
26+
27+
contract ETHRegistrar is IETHRegistrar, EnhancedAccessControl {
28+
////////////////////////////////////////////////////////////////////////
29+
// Constants
30+
////////////////////////////////////////////////////////////////////////
31+
32+
IPermissionedRegistry public immutable REGISTRY;
33+
34+
address public immutable BENEFICIARY;
35+
36+
uint64 public immutable MIN_COMMITMENT_AGE;
37+
38+
uint64 public immutable MAX_COMMITMENT_AGE;
39+
40+
uint64 public immutable MIN_REGISTER_DURATION;
41+
42+
////////////////////////////////////////////////////////////////////////
43+
// Storage
44+
////////////////////////////////////////////////////////////////////////
45+
46+
IRentPriceOracle public rentPriceOracle;
47+
48+
/// @inheritdoc IETHRegistrar
49+
mapping(bytes32 commitment => uint64 commitTime) public commitmentAt;
50+
51+
////////////////////////////////////////////////////////////////////////
52+
// Events
53+
////////////////////////////////////////////////////////////////////////
54+
55+
event RentPriceOracleChanged(IRentPriceOracle oracle);
56+
57+
////////////////////////////////////////////////////////////////////////
58+
// Initialization
59+
////////////////////////////////////////////////////////////////////////
60+
61+
constructor(
62+
IPermissionedRegistry registry,
63+
IHCAFactoryBasic hcaFactory,
64+
address beneficiary,
65+
uint64 minCommitmentAge,
66+
uint64 maxCommitmentAge,
67+
uint64 minRegisterDuration,
68+
IRentPriceOracle rentPriceOracle_
69+
) HCAEquivalence(hcaFactory) {
70+
if (maxCommitmentAge <= minCommitmentAge) {
71+
revert MaxCommitmentAgeTooLow();
72+
}
73+
_grantRoles(ROOT_RESOURCE, EACBaseRolesLib.ALL_ROLES, _msgSender(), true);
74+
75+
REGISTRY = registry;
76+
BENEFICIARY = beneficiary;
77+
MIN_COMMITMENT_AGE = minCommitmentAge;
78+
MAX_COMMITMENT_AGE = maxCommitmentAge;
79+
MIN_REGISTER_DURATION = minRegisterDuration;
80+
81+
rentPriceOracle = rentPriceOracle_;
82+
emit RentPriceOracleChanged(rentPriceOracle_);
83+
}
84+
85+
/// @inheritdoc EnhancedAccessControl
86+
function supportsInterface(
87+
bytes4 interfaceId
88+
) public view override(EnhancedAccessControl) returns (bool) {
89+
return
90+
interfaceId == type(IETHRegistrar).interfaceId ||
91+
interfaceId == type(IRentPriceOracle).interfaceId ||
92+
super.supportsInterface(interfaceId);
93+
}
94+
95+
////////////////////////////////////////////////////////////////////////
96+
// Implementation
97+
////////////////////////////////////////////////////////////////////////
98+
99+
/// @dev Change the rent price oracle.
100+
function setRentPriceOracle(IRentPriceOracle oracle) external onlyRootRoles(ROLE_SET_ORACLE) {
101+
rentPriceOracle = oracle;
102+
emit RentPriceOracleChanged(oracle);
103+
}
104+
105+
/// @inheritdoc IETHRegistrar
106+
function commit(bytes32 commitment) external {
107+
if (commitmentAt[commitment] + MAX_COMMITMENT_AGE > block.timestamp) {
108+
revert UnexpiredCommitmentExists(commitment);
109+
}
110+
commitmentAt[commitment] = uint64(block.timestamp);
111+
emit CommitmentMade(commitment);
112+
}
113+
114+
/// @inheritdoc IETHRegistrar
115+
function register(
116+
string calldata label,
117+
address owner,
118+
bytes32 secret,
119+
IRegistry subregistry,
120+
address resolver,
121+
uint64 duration,
122+
IERC20 paymentToken,
123+
bytes32 referrer
124+
) external returns (uint256 tokenId) {
125+
(, IRegistryDatastore.Entry memory entry) = REGISTRY.getNameData(label);
126+
uint64 oldExpiry = entry.expiry;
127+
if (!_isAvailable(oldExpiry)) {
128+
revert NameAlreadyRegistered(label);
129+
}
130+
if (duration < MIN_REGISTER_DURATION) {
131+
revert DurationTooShort(duration, MIN_REGISTER_DURATION);
132+
}
133+
_consumeCommitment(
134+
makeCommitment(label, owner, secret, subregistry, resolver, duration, referrer)
135+
);
136+
(uint256 base, uint256 premium) = rentPrice(label, owner, duration, paymentToken); // reverts if !isValid or !isPaymentToken
137+
SafeERC20.safeTransferFrom(paymentToken, _msgSender(), BENEFICIARY, base + premium); // reverts if payment failed
138+
tokenId = REGISTRY.register(
139+
label,
140+
owner,
141+
subregistry,
142+
resolver,
143+
REGISTRATION_ROLE_BITMAP,
144+
uint64(block.timestamp) + duration
145+
); // reverts if owner is null
146+
emit NameRegistered(
147+
tokenId,
148+
label,
149+
owner,
150+
subregistry,
151+
resolver,
152+
duration,
153+
paymentToken,
154+
referrer,
155+
base,
156+
premium
157+
);
158+
}
159+
160+
/// @inheritdoc IETHRegistrar
161+
function renew(
162+
string calldata label,
163+
uint64 duration,
164+
IERC20 paymentToken,
165+
bytes32 referrer
166+
) external {
167+
(uint256 tokenId, IRegistryDatastore.Entry memory entry) = REGISTRY.getNameData(label);
168+
uint64 oldExpiry = entry.expiry;
169+
if (_isAvailable(oldExpiry)) {
170+
revert NameNotRegistered(label);
171+
}
172+
uint64 expiry = oldExpiry + duration;
173+
(uint256 base, ) = rentPrice(
174+
label,
175+
REGISTRY.latestOwnerOf(tokenId),
176+
duration,
177+
paymentToken
178+
); // reverts if !isValid or !isPaymentToken or duration is 0
179+
SafeERC20.safeTransferFrom(paymentToken, _msgSender(), BENEFICIARY, base); // reverts if payment failed
180+
REGISTRY.renew(tokenId, expiry);
181+
emit NameRenewed(tokenId, label, duration, expiry, paymentToken, referrer, base);
182+
}
183+
184+
/// @inheritdoc IRentPriceOracle
185+
function isPaymentToken(IERC20 paymentToken) external view returns (bool) {
186+
return rentPriceOracle.isPaymentToken(paymentToken);
187+
}
188+
189+
/// @inheritdoc IRentPriceOracle
190+
function isValid(string calldata label) external view returns (bool) {
191+
return rentPriceOracle.isValid(label);
192+
}
193+
194+
/// @inheritdoc IETHRegistrar
195+
/// @dev Does not check if normalized or valid.
196+
function isAvailable(string calldata label) external view returns (bool) {
197+
(, IRegistryDatastore.Entry memory entry) = REGISTRY.getNameData(label);
198+
uint64 expiry = entry.expiry;
199+
return _isAvailable(expiry);
200+
}
201+
202+
/// @inheritdoc IRentPriceOracle
203+
function rentPrice(
204+
string memory label,
205+
address owner,
206+
uint64 duration,
207+
IERC20 paymentToken
208+
) public view returns (uint256 base, uint256 premium) {
209+
return rentPriceOracle.rentPrice(label, owner, duration, paymentToken);
210+
}
211+
212+
/// @inheritdoc IETHRegistrar
213+
function makeCommitment(
214+
string calldata label,
215+
address owner,
216+
bytes32 secret,
217+
IRegistry subregistry,
218+
address resolver,
219+
uint64 duration,
220+
bytes32 referrer
221+
) public pure override returns (bytes32) {
222+
return
223+
keccak256(abi.encode(label, owner, secret, subregistry, resolver, duration, referrer));
224+
}
225+
226+
////////////////////////////////////////////////////////////////////////
227+
// Internal Functions
228+
////////////////////////////////////////////////////////////////////////
229+
230+
/// @dev Assert `commitment` is timely, then delete it.
231+
function _consumeCommitment(bytes32 commitment) internal {
232+
uint64 t = uint64(block.timestamp);
233+
uint64 t0 = commitmentAt[commitment];
234+
uint64 tMin = t0 + MIN_COMMITMENT_AGE;
235+
if (t < tMin) {
236+
revert CommitmentTooNew(commitment, tMin, t);
237+
}
238+
uint64 tMax = t0 + MAX_COMMITMENT_AGE;
239+
if (t >= tMax) {
240+
revert CommitmentTooOld(commitment, tMax, t);
241+
}
242+
delete commitmentAt[commitment];
243+
}
244+
245+
/// @dev Internal logic for registration availability.
246+
function _isAvailable(uint256 expiry) internal view returns (bool) {
247+
return block.timestamp >= expiry;
248+
}
249+
}

0 commit comments

Comments
 (0)