Skip to content

Commit 0c8c253

Browse files
committed
feat: add pegin address registry
1 parent 86542ef commit 0c8c253

7 files changed

Lines changed: 956 additions & 0 deletions

src/PegInAddressRegistry.sol

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import {
5+
AccessControlDefaultAdminRulesUpgradeable
6+
} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol";
7+
import {BtcUtils} from "@rsksmart/btc-transaction-solidity-helper/contracts/BtcUtils.sol";
8+
import {OpCodes} from "@rsksmart/btc-transaction-solidity-helper/contracts/OpCodes.sol";
9+
import {EmergencyPause} from "./EmergencyPause/EmergencyPause.sol";
10+
import {IBridge} from "./interfaces/IBridge.sol";
11+
import {IPauseRegistry} from "./interfaces/IPauseRegistry.sol";
12+
import {IPegInAddressRegistry} from "./interfaces/IPegInAddressRegistry.sol";
13+
import {Flyover} from "./libraries/Flyover.sol";
14+
15+
/// @title PegInAddressRegistry
16+
/// @notice Maintains registered Flyover peg-in Rootstock addresses and derives the
17+
/// corresponding Bitcoin deposit addresses for the current powpeg composition
18+
contract PegInAddressRegistry is
19+
AccessControlDefaultAdminRulesUpgradeable,
20+
EmergencyPause,
21+
IPegInAddressRegistry
22+
{
23+
struct Registration {
24+
uint256 blockNumber;
25+
address registrant;
26+
}
27+
28+
/// @notice The version of the contract
29+
string public constant VERSION = "1.0.0";
30+
31+
IBridge private _bridge;
32+
bool private _mainnet;
33+
34+
bytes32 private _registrationRoot;
35+
uint256 private _registrationCount;
36+
mapping(address => Registration) private _registrations;
37+
38+
/// @notice Emitted when an address is already registered
39+
/// @param addr The Rootstock address that was already registered
40+
error AlreadyRegistered(address addr);
41+
42+
/// @custom:oz-upgrades-unsafe-allow constructor
43+
constructor() {
44+
_disableInitializers();
45+
}
46+
47+
/// @notice Initializes the contract
48+
/// @param defaultAdmin The default admin of the contract
49+
/// @param bridge The Rootstock bridge contract
50+
/// @param mainnet Whether the contract derives mainnet or testnet BTC addresses
51+
/// @param pauseRegistry The central PauseRegistry for pause state
52+
// solhint-disable-next-line comprehensive-interface
53+
function initialize(
54+
address defaultAdmin,
55+
address payable bridge,
56+
bool mainnet,
57+
address pauseRegistry
58+
) external initializer {
59+
if (address(pauseRegistry).code.length == 0) {
60+
revert Flyover.NoContract(address(pauseRegistry));
61+
}
62+
__AccessControlDefaultAdminRules_init(0, defaultAdmin);
63+
__EmergencyPause_init(IPauseRegistry(pauseRegistry));
64+
_bridge = IBridge(bridge);
65+
_mainnet = mainnet;
66+
}
67+
68+
/// @inheritdoc IPegInAddressRegistry
69+
function registerAddress(address addr) external override whenNotSoftPaused {
70+
if (addr == address(0)) revert Flyover.InvalidAddress(addr);
71+
if (_isRegistered(addr)) revert AlreadyRegistered(addr);
72+
73+
_registrations[addr] = Registration({
74+
blockNumber: block.number,
75+
registrant: msg.sender
76+
});
77+
++_registrationCount;
78+
_registrationRoot = keccak256(abi.encodePacked(_registrationRoot, addr));
79+
80+
emit AddressRegistered(addr, msg.sender, _registrationRoot);
81+
}
82+
83+
/// @inheritdoc IPegInAddressRegistry
84+
function getPegInAddress(
85+
address addr
86+
) external view override returns (bytes memory derivationAddress, Encoding encoding) {
87+
return (_derivePegInAddress(addr), Encoding.BASE58);
88+
}
89+
90+
/// @inheritdoc IPegInAddressRegistry
91+
function getPegInAddresses(
92+
address[] calldata addrs
93+
) external view override returns (bytes[] memory derivationAddresses, Encoding encoding) {
94+
uint addressCount = addrs.length;
95+
derivationAddresses = new bytes[](addressCount);
96+
for (uint256 i = 0; i < addressCount; ++i) {
97+
derivationAddresses[i] = _derivePegInAddress(addrs[i]);
98+
}
99+
return (derivationAddresses, Encoding.BASE58);
100+
}
101+
102+
/// @inheritdoc IPegInAddressRegistry
103+
function getRegistrationRoot() external view override returns (bytes32) {
104+
return _registrationRoot;
105+
}
106+
107+
/// @inheritdoc IPegInAddressRegistry
108+
function isRegistered(address addr) external view override returns (bool) {
109+
return _isRegistered(addr);
110+
}
111+
112+
/// @inheritdoc IPegInAddressRegistry
113+
function getRegistrationBlock(address addr) external view override returns (uint256) {
114+
return _registrations[addr].blockNumber;
115+
}
116+
117+
/// @inheritdoc IPegInAddressRegistry
118+
function getRegistrant(address addr) external view override returns (address) {
119+
return _registrations[addr].registrant;
120+
}
121+
122+
/// @inheritdoc IPegInAddressRegistry
123+
function getRegistrationCount() external view override returns (uint256) {
124+
return _registrationCount;
125+
}
126+
127+
function _isRegistered(address addr) private view returns (bool) {
128+
return _registrations[addr].blockNumber != 0;
129+
}
130+
131+
/// @notice Derives the Bitcoin P2SH deposit address for a Rootstock address
132+
/// @dev Builds the Flyover redeem script from the address hash and the active powpeg
133+
/// redeem script, then wraps it in a P2WSH-in-P2SH script as in `PegInContract`.
134+
function _derivePegInAddress(address addr) private view returns (bytes memory) {
135+
bytes32 derivationValue = keccak256(abi.encodePacked(addr));
136+
bytes memory flyoverRedeemScript = bytes.concat(
137+
OpCodes.OP_PUSHBYTES_32,
138+
derivationValue,
139+
OpCodes.OP_DROP,
140+
_bridge.getActivePowpegRedeemScript()
141+
);
142+
bytes memory segwitScript = bytes.concat(
143+
OpCodes.OP_0,
144+
OpCodes.OP_PUSHBYTES_32,
145+
sha256(flyoverRedeemScript)
146+
);
147+
return BtcUtils.getP2SHAddressFromScript(segwitScript, _mainnet);
148+
}
149+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
/// @title Peg-In Address Registry interface
5+
/// @notice Maintains registered Flyover peg-in Rootstock addresses and returns the
6+
/// corresponding Bitcoin derivation addresses for the current powpeg composition.
7+
interface IPegInAddressRegistry {
8+
/// @notice Bitcoin address encoding formats supported for derivation addresses
9+
enum Encoding { BASE58, BECH32, BECH32M }
10+
11+
/// @notice Emitted when a Rootstock address is registered for peg-in discovery
12+
/// @param addr The registered Rootstock destination address
13+
/// @param registrant The account that submitted the registration
14+
/// @param registrationRoot The updated running hash after this registration
15+
event AddressRegistered(
16+
address indexed addr,
17+
address indexed registrant,
18+
bytes32 indexed registrationRoot
19+
);
20+
21+
/// @notice Registers a Rootstock address for peg-in discovery
22+
/// @param addr The Rootstock destination address to register
23+
function registerAddress(address addr) external;
24+
25+
/// @notice Returns the Bitcoin derivation address for a Rootstock address
26+
/// @dev The derivation address is deterministic from the Rootstock address and the
27+
/// current powpeg composition. Clients call this before sending BTC; LPs use it to
28+
/// know which deposit addresses to monitor on the Bitcoin network
29+
/// @param addr The Rootstock destination address
30+
/// @return derivationAddress The encoded Bitcoin deposit address
31+
/// @return encoding The encoding format of the derivation address
32+
function getPegInAddress(address addr) external view returns (bytes memory derivationAddress, Encoding encoding);
33+
34+
/// @notice Returns Bitcoin derivation addresses for multiple Rootstock addresses
35+
/// @dev Is the array version of `getPegInAddress`
36+
/// @param addrs The Rootstock destination addresses to derive
37+
/// @return derivationAddresses The encoded Bitcoin deposit addresses
38+
/// @return encoding The encoding format shared by all derivation addresses
39+
function getPegInAddresses(address[] calldata addrs)
40+
external
41+
view
42+
returns (bytes[] memory derivationAddresses, Encoding encoding);
43+
44+
/// @notice Returns the on-chain running hash of all registrations
45+
/// @dev Off-chain LPs compare this value against a hash computed locally from
46+
/// `AddressRegistered` events to verify they hold the complete registration set
47+
/// @return The current `registrationRoot` accumulator
48+
function getRegistrationRoot() external view returns (bytes32);
49+
50+
/// @notice Returns whether a Rootstock address is registered
51+
/// @dev Used by on-chain peg-in logic to require registration before processing
52+
/// @param addr The Rootstock destination address to check
53+
/// @return True if the address is registered
54+
function isRegistered(address addr) external view returns (bool);
55+
56+
/// @notice Returns the block number at which a Rootstock address was registered
57+
/// @param addr The Rootstock destination address
58+
/// @return The block number of the registration transaction
59+
function getRegistrationBlock(address addr) external view returns (uint256);
60+
61+
/// @notice Returns the account that registered a Rootstock address
62+
/// @param addr The Rootstock destination address
63+
/// @return The registrant address, or `address(0)` if not registered
64+
function getRegistrant(address addr) external view returns (address);
65+
66+
/// @notice Returns the total number of registered Rootstock addresses
67+
/// @return The count of addresses in the registry
68+
function getRegistrationCount() external view returns (uint256);
69+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import {PegInAddressRegistryFuzzTestBase} from "./PegInAddressRegistryFuzzTestBase.sol";
5+
import {PegInAddressRegistry} from "../../../src/PegInAddressRegistry.sol";
6+
import {IPegInAddressRegistry} from "../../../src/interfaces/IPegInAddressRegistry.sol";
7+
import {Flyover} from "../../../src/libraries/Flyover.sol";
8+
9+
/// @title PegInAddressRegistry Fuzz Tests
10+
contract PegInAddressRegistryFuzzTest is PegInAddressRegistryFuzzTestBase {
11+
function setUp() public {
12+
deployPegInAddressRegistry();
13+
}
14+
15+
// ============ Registration fuzz tests ============
16+
17+
function testFuzz_Register_RevertsOnZeroAddress(uint256) public {
18+
vm.expectRevert(
19+
abi.encodeWithSelector(Flyover.InvalidAddress.selector, address(0))
20+
);
21+
registry.registerAddress(address(0));
22+
}
23+
24+
function testFuzz_Register_RevertsWhenAlreadyRegistered(
25+
uint256 seed
26+
) public {
27+
address addr = address(uint160(seed));
28+
vm.assume(addr != address(0));
29+
30+
registry.registerAddress(addr);
31+
32+
vm.expectRevert(
33+
abi.encodeWithSelector(
34+
PegInAddressRegistry.AlreadyRegistered.selector,
35+
addr
36+
)
37+
);
38+
registry.registerAddress(addr);
39+
}
40+
41+
function testFuzz_Register_StoresRegistrant(
42+
uint256 addrSeed,
43+
uint256 registrantSeed
44+
) public {
45+
address addr = address(uint160(addrSeed));
46+
address registrant = address(uint160(registrantSeed));
47+
vm.assume(addr != address(0));
48+
vm.assume(registrant != address(0));
49+
50+
vm.prank(registrant);
51+
registry.registerAddress(addr);
52+
53+
assertEq(registry.getRegistrant(addr), registrant);
54+
}
55+
56+
function testFuzz_RegistrationRoot_MatchesOrderedReplay(
57+
uint256 seed,
58+
uint256 count
59+
) public {
60+
address[] memory addrs = generateUniqueAddresses(seed, count);
61+
for (uint256 i = 0; i < addrs.length; ++i) {
62+
registry.registerAddress(addrs[i]);
63+
}
64+
65+
assertEq(registry.getRegistrationCount(), addrs.length);
66+
assertEq(
67+
registry.getRegistrationRoot(),
68+
computeRegistrationRoot(addrs)
69+
);
70+
}
71+
72+
// ============ Derivation fuzz tests ============
73+
74+
function testFuzz_GetPegInAddress_Deterministic(uint256 seed) public view {
75+
address addr = address(uint160(seed));
76+
77+
(bytes memory first, ) = registry.getPegInAddress(addr);
78+
(bytes memory second, ) = registry.getPegInAddress(addr);
79+
80+
assertEq(keccak256(first), keccak256(second));
81+
}
82+
83+
function testFuzz_GetPegInAddress_OutputLength25(uint256 seed) public view {
84+
address addr = address(uint160(seed));
85+
(bytes memory derivationAddress, ) = registry.getPegInAddress(addr);
86+
assertEq(derivationAddress.length, 25);
87+
}
88+
89+
function testFuzz_GetPegInAddress_IndependentOfRegistration(
90+
uint256 seed
91+
) public {
92+
address addr = address(uint160(seed));
93+
vm.assume(addr != address(0));
94+
95+
(
96+
bytes memory before,
97+
IPegInAddressRegistry.Encoding encodingBefore
98+
) = registry.getPegInAddress(addr);
99+
100+
registry.registerAddress(addr);
101+
102+
(
103+
bytes memory afterReg,
104+
IPegInAddressRegistry.Encoding encodingAfter
105+
) = registry.getPegInAddress(addr);
106+
107+
assertEq(keccak256(before), keccak256(afterReg));
108+
assertEq(
109+
uint8(encodingBefore),
110+
uint8(IPegInAddressRegistry.Encoding.BASE58)
111+
);
112+
assertEq(
113+
uint8(encodingAfter),
114+
uint8(IPegInAddressRegistry.Encoding.BASE58)
115+
);
116+
}
117+
118+
function testFuzz_GetPegInAddresses_MatchesSingle(
119+
uint256 seed,
120+
uint256 count
121+
) public view {
122+
address[] memory addrs = generateUniqueAddresses(seed, count);
123+
124+
(
125+
bytes[] memory batch,
126+
IPegInAddressRegistry.Encoding batchEncoding
127+
) = registry.getPegInAddresses(addrs);
128+
129+
assertEq(batch.length, addrs.length);
130+
for (uint256 i = 0; i < addrs.length; ++i) {
131+
(
132+
bytes memory single,
133+
IPegInAddressRegistry.Encoding singleEncoding
134+
) = registry.getPegInAddress(addrs[i]);
135+
assertEq(keccak256(batch[i]), keccak256(single));
136+
assertEq(uint8(batchEncoding), uint8(singleEncoding));
137+
}
138+
assertEq(
139+
uint8(batchEncoding),
140+
uint8(IPegInAddressRegistry.Encoding.BASE58)
141+
);
142+
}
143+
144+
function testFuzz_GetPegInAddress_DifferentAddrsDifferentOutput(
145+
uint256 seedA,
146+
uint256 seedB
147+
) public view {
148+
address addrA = address(uint160(seedA));
149+
address addrB = address(uint160(seedB));
150+
vm.assume(addrA != addrB);
151+
152+
(bytes memory derivationA, ) = registry.getPegInAddress(addrA);
153+
(bytes memory derivationB, ) = registry.getPegInAddress(addrB);
154+
155+
assertTrue(keccak256(derivationA) != keccak256(derivationB));
156+
}
157+
}

0 commit comments

Comments
 (0)