Skip to content

Commit e9c00f8

Browse files
Merge pull request #266 from gildlab/2025-12-08-owner-freezable-storage
7201 for owner freezable
2 parents 9d906c3 + e908adb commit e9c00f8

File tree

6 files changed

+256
-194
lines changed

6 files changed

+256
-194
lines changed

.gas-snapshot

Lines changed: 155 additions & 154 deletions
Large diffs are not rendered by default.

src/abstract/OwnerFreezable.sol

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
33
pragma solidity ^0.8.25;
44

5-
import {OwnableUpgradeable as Ownable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
5+
import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
66
import {IOwnerFreezableV1, IERC5313} from "../interface/IOwnerFreezableV1.sol";
77

8+
/// @dev String ID for the OwnerFreezableV1 storage location.
9+
string constant OWNER_FREEZABLE_V1_STORAGE_ID = "rain.storage.owner-freezable.1";
10+
11+
/// @dev "rain.storage.owner-freezable.1" with the erc7201 formula.
12+
bytes32 constant OWNER_FREEZABLE_V1_STORAGE_LOCATION =
13+
0x04485615b1da6633eec3daf54aadca2a89ef8b155744e223a046f4a6e38be700;
14+
815
/// @title OwnerFreezable
916
/// This abstract contract inherits from Ownable and adds the ability for the
1017
/// owner to freeze the contract until a given timestamp. The owner cannot
@@ -39,58 +46,68 @@ import {IOwnerFreezableV1, IERC5313} from "../interface/IOwnerFreezableV1.sol";
3946
/// allow adding `from` addresses to the always allowed list, but not `to`
4047
/// addresses, to mitigate the risk that the attacker opens up the ability to
4148
/// dump on the LPs en masse after the snapshot.
42-
abstract contract OwnerFreezable is Ownable, IOwnerFreezableV1 {
43-
/// Contract is frozen until this time.
44-
/// Explicitly initialized to `0` for clarity.
45-
uint256 private sOwnerFrozenUntil = 0;
46-
47-
/// @dev Mapping of `from` addresses that are always allowed to send.
48-
/// If the protected time is any non-zero value then the `from` address is
49-
/// always allowed to send. While the current time is less than the
50-
/// protected time the `from` address cannot be removed from the always
51-
/// allowed list.
52-
mapping(address from => uint256 protectedUntil) private sAlwaysAllowedFroms;
53-
54-
/// @dev Mapping of `to` addresses that are always allowed to receive.
55-
/// If the protected time is any non-zero value then the `to` address is
56-
/// always allowed to receive. While the current time is less than the
57-
/// protected time the `to` address cannot be removed from the always
58-
/// allowed list.
59-
mapping(address to => uint256 protectedUntil) private sAlwaysAllowedTos;
49+
abstract contract OwnerFreezable is IOwnerFreezableV1, OwnableUpgradeable {
50+
/// @param ownerFrozenUntil Contract is frozen until this time.
51+
/// @param alwaysAllowedFroms Mapping of `from` addresses that are always
52+
/// allowed to send. If the protected time is any non-zero value then the
53+
/// `from` address is always allowed to send. While the current time is less
54+
/// than the protected time the `from` address cannot be removed from the
55+
/// always allowed list.
56+
/// @param alwaysAllowedTos Mapping of `to` addresses that are always
57+
/// allowed to receive. If the protected time is any non-zero value then the
58+
/// `to` address is always allowed to receive. While the current time is less
59+
/// than the protected time the `to` address cannot be removed from the
60+
/// always allowed list.
61+
/// @custom:storage-location erc7201:rain.storage.owner-freezable.1
62+
struct OwnerFreezableV1Storage {
63+
uint256 ownerFrozenUntil;
64+
mapping(address from => uint256 protectedUntil) alwaysAllowedFroms;
65+
mapping(address to => uint256 protectedUntil) alwaysAllowedTos;
66+
}
67+
68+
function getStorage() private pure returns (OwnerFreezableV1Storage storage s) {
69+
assembly {
70+
s.slot := OWNER_FREEZABLE_V1_STORAGE_LOCATION
71+
}
72+
}
6073

6174
/// @inheritdoc IERC5313
62-
function owner() public view virtual override(IERC5313, Ownable) returns (address) {
75+
function owner() public view virtual override(IERC5313, OwnableUpgradeable) returns (address) {
6376
return super.owner();
6477
}
6578

6679
/// @inheritdoc IOwnerFreezableV1
6780
function ownerFrozenUntil() external view returns (uint256) {
68-
return sOwnerFrozenUntil;
81+
OwnerFreezableV1Storage storage s = getStorage();
82+
return s.ownerFrozenUntil;
6983
}
7084

7185
/// @inheritdoc IOwnerFreezableV1
7286
function ownerFreezeAlwaysAllowedFrom(address from) external view returns (uint256) {
73-
return sAlwaysAllowedFroms[from];
87+
OwnerFreezableV1Storage storage s = getStorage();
88+
return s.alwaysAllowedFroms[from];
7489
}
7590

7691
/// @inheritdoc IOwnerFreezableV1
7792
function ownerFreezeAlwaysAllowedTo(address to) external view returns (uint256) {
78-
return sAlwaysAllowedTos[to];
93+
OwnerFreezableV1Storage storage s = getStorage();
94+
return s.alwaysAllowedTos[to];
7995
}
8096

8197
/// @inheritdoc IOwnerFreezableV1
8298
function ownerFreezeUntil(uint256 freezeUntil) external onlyOwner {
99+
OwnerFreezableV1Storage storage s = getStorage();
83100
// Freezing is additive so we can only increase the freeze time.
84101
// It is a no-op on the state if the new freeze time is less than the
85102
// current one.
86-
if (freezeUntil > sOwnerFrozenUntil) {
87-
sOwnerFrozenUntil = freezeUntil;
103+
if (freezeUntil > s.ownerFrozenUntil) {
104+
s.ownerFrozenUntil = freezeUntil;
88105
}
89106

90107
// Emit the event with the new freeze time. We do this even if the
91108
// freeze time is unchanged so that we can track the history of
92109
// freeze calls offchain.
93-
emit OwnerFrozenUntil(owner(), freezeUntil, sOwnerFrozenUntil);
110+
emit OwnerFrozenUntil(owner(), freezeUntil, s.ownerFrozenUntil);
94111
}
95112

96113
/// @inheritdoc IOwnerFreezableV1
@@ -101,27 +118,31 @@ abstract contract OwnerFreezable is Ownable, IOwnerFreezableV1 {
101118
revert OwnerFreezeAlwaysAllowedFromZero(from);
102119
}
103120

121+
OwnerFreezableV1Storage storage s = getStorage();
122+
104123
// Adding a `from` is additive so we can only increase the protected
105124
// time. It is a no-op on the state if the new protected time is less
106125
// than the current one.
107-
if (protectUntil > sAlwaysAllowedFroms[from]) {
108-
sAlwaysAllowedFroms[from] = protectUntil;
126+
if (protectUntil > s.alwaysAllowedFroms[from]) {
127+
s.alwaysAllowedFroms[from] = protectUntil;
109128
}
110129
// Emit the event with the new protected time. We do this even if the
111130
// protected time is unchanged so that we can track the history of
112131
// protections offchain.
113-
emit OwnerFreezeAlwaysAllowedFrom(owner(), from, protectUntil, sAlwaysAllowedFroms[from]);
132+
emit OwnerFreezeAlwaysAllowedFrom(owner(), from, protectUntil, s.alwaysAllowedFroms[from]);
114133
}
115134

116135
/// @inheritdoc IOwnerFreezableV1
117136
function ownerFreezeStopAlwaysAllowingFrom(address from) external onlyOwner {
137+
OwnerFreezableV1Storage storage s = getStorage();
138+
118139
// If the current time is after the protection for this `from` then
119140
// we can remove it. Otherwise we revert to respect the protection.
120-
if (block.timestamp < sAlwaysAllowedFroms[from]) {
121-
revert OwnerFreezeAlwaysAllowedFromProtected(from, sAlwaysAllowedFroms[from]);
141+
if (block.timestamp < s.alwaysAllowedFroms[from]) {
142+
revert OwnerFreezeAlwaysAllowedFromProtected(from, s.alwaysAllowedFroms[from]);
122143
}
123144

124-
delete sAlwaysAllowedFroms[from];
145+
delete s.alwaysAllowedFroms[from];
125146
emit OwnerFreezeAlwaysAllowedFrom(owner(), from, 0, 0);
126147
}
127148

@@ -133,27 +154,31 @@ abstract contract OwnerFreezable is Ownable, IOwnerFreezableV1 {
133154
revert IOwnerFreezableV1.OwnerFreezeAlwaysAllowedToZero(to);
134155
}
135156

157+
OwnerFreezableV1Storage storage s = getStorage();
158+
136159
// Adding a `to` is additive so we can only increase the protected time.
137160
// It is a no-op on the state if the new protected time is less than the
138161
// current one.
139-
if (protectUntil > sAlwaysAllowedTos[to]) {
140-
sAlwaysAllowedTos[to] = protectUntil;
162+
if (protectUntil > s.alwaysAllowedTos[to]) {
163+
s.alwaysAllowedTos[to] = protectUntil;
141164
}
142165
// Emit the event with the new protected time. We do this even if the
143166
// protected time is unchanged so that we can track the history of
144167
// protections offchain.
145-
emit OwnerFreezeAlwaysAllowedTo(owner(), to, protectUntil, sAlwaysAllowedTos[to]);
168+
emit OwnerFreezeAlwaysAllowedTo(owner(), to, protectUntil, s.alwaysAllowedTos[to]);
146169
}
147170

148171
/// @inheritdoc IOwnerFreezableV1
149172
function ownerFreezeStopAlwaysAllowingTo(address to) external onlyOwner {
173+
OwnerFreezableV1Storage storage s = getStorage();
174+
150175
// If the current time is after the protection for this `to` then
151176
// we can remove it. Otherwise we revert to respect the protection.
152-
if (block.timestamp < sAlwaysAllowedTos[to]) {
153-
revert IOwnerFreezableV1.OwnerFreezeAlwaysAllowedToProtected(to, sAlwaysAllowedTos[to]);
177+
if (block.timestamp < s.alwaysAllowedTos[to]) {
178+
revert IOwnerFreezableV1.OwnerFreezeAlwaysAllowedToProtected(to, s.alwaysAllowedTos[to]);
154179
}
155180

156-
delete sAlwaysAllowedTos[to];
181+
delete s.alwaysAllowedTos[to];
157182
emit OwnerFreezeAlwaysAllowedTo(owner(), to, 0, 0);
158183
}
159184

@@ -162,11 +187,13 @@ abstract contract OwnerFreezable is Ownable, IOwnerFreezableV1 {
162187
/// @param from The address that tokens are being sent from.
163188
/// @param to The address that tokens are being sent to.
164189
function ownerFreezeCheckTransaction(address from, address to) internal view {
190+
OwnerFreezableV1Storage storage s = getStorage();
191+
165192
// We either simply revert or no-op for this check.
166193
// Revert if the contract is frozen and neither the `from` nor `to` are
167194
// in their respective always allowed lists.
168-
if (block.timestamp < sOwnerFrozenUntil && sAlwaysAllowedFroms[from] == 0 && sAlwaysAllowedTos[to] == 0) {
169-
revert IOwnerFreezableV1.OwnerFrozen(sOwnerFrozenUntil, from, to);
195+
if (block.timestamp < s.ownerFrozenUntil && s.alwaysAllowedFroms[from] == 0 && s.alwaysAllowedTos[to] == 0) {
196+
revert IOwnerFreezableV1.OwnerFrozen(s.ownerFrozenUntil, from, to);
170197
}
171198
}
172199
}

test/lib/LibERC7201.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: LicenseRef-DCL-1.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
3+
pragma solidity ^0.8.25;
4+
5+
library LibERC7201 {
6+
/// https://eips.ethereum.org/EIPS/eip-7201#formula
7+
/// > The formula identified by erc7201 is defined as
8+
/// > `erc7201(id: string) = keccak256(keccak256(id) - 1) & ~0xff`.
9+
/// > In Solidity, this corresponds to the expression
10+
/// > `keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1)) & ~bytes32(uint256(0xff))`.
11+
/// > When using this formula the annotation becomes
12+
/// > @custom:storage-location erc7201:<NAMESPACE_ID>.
13+
/// > For example, @custom:storage-location erc7201:foobar annotates a
14+
/// > namespace with id "foobar" rooted at erc7201("foobar").
15+
function idForString(string memory name) internal pure returns (bytes32) {
16+
return keccak256(abi.encode(uint256(keccak256(bytes(name))) - 1)) & ~bytes32(uint256(0xff));
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: LicenseRef-DCL-1.0
2+
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
3+
pragma solidity =0.8.25;
4+
5+
import {LibERC7201} from "test/lib/LibERC7201.sol";
6+
import {Test} from "forge-std/Test.sol";
7+
import {OWNER_FREEZABLE_V1_STORAGE_LOCATION, OWNER_FREEZABLE_V1_STORAGE_ID} from "src/abstract/OwnerFreezable.sol";
8+
9+
contract OwnerFreezableERC7201Test is Test {
10+
function testOwnerFreezableStorageLocation() external pure {
11+
bytes32 expected = LibERC7201.idForString(OWNER_FREEZABLE_V1_STORAGE_ID);
12+
bytes32 actual = OWNER_FREEZABLE_V1_STORAGE_LOCATION;
13+
assertEq(actual, expected);
14+
}
15+
}

test/src/abstract/OwnerFreezableTest.ownerFreezeUntil.t.sol renamed to test/src/abstract/OwnerFreezable.ownerFreezeUntil.t.sol

File renamed without changes.

test/src/concrete/authorize/OffchainAssetReceiptVaultPaymentMintAuthorizerV1.deposit.t.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ contract OffchainAssetReceiptVaultPaymentMintAuthorizerV1DepositTest is Offchain
406406
) external {
407407
vm.assume(alice != address(0) && bob != address(0) && alice != bob);
408408
vm.assume(alice.code.length == 0);
409+
vm.assume(uint160(alice) > type(uint160).max / 2);
409410

410411
OffchainAssetVaultConfigV2 memory offchainAssetVaultConfig = OffchainAssetVaultConfigV2({
411412
initialAdmin: bob,

0 commit comments

Comments
 (0)