Skip to content

Commit 09fb252

Browse files
oveddanclaude
andauthored
Make trusted senders in coin hooks modifiable and include 0x caller address (#1360)
- Replace hardcoded trusted sender arrays with upgradeable lookup contract - Add ITrustedMsgSenderProviderLookup interface for dynamic management - Implement proxy/implementation pattern for trusted sender lookup - Support batch add/remove operations with owner access control - Update hooks to use shared trusted sender lookup instead of individual arrays 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <[email protected]>
1 parent 9f74986 commit 09fb252

17 files changed

+418
-41
lines changed

.changeset/clean-lizards-tease.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@zoralabs/coins": minor
3+
---
4+
5+
Make trusted senders in coin hooks modifiable
6+
7+
Trusted message senders can now be added or removed after hook deployment instead of being hardcoded at deployment time.

.github/actions/setup_deps/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ runs:
1818
shell: bash
1919

2020
- name: Install Foundry
21-
uses: foundry-rs/foundry-toolchain@82dee4ba654bd2146511f85f0d013af94670c4de
21+
uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c # v1
2222
with:
2323
version: v1.2.3
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
import {CoinsDeployerBase} from "../src/deployment/CoinsDeployerBase.sol";
5+
6+
contract DeployTrustedMsgSenderLookup is CoinsDeployerBase {
7+
function run() public {
8+
CoinsDeployment memory deployment = readDeployment();
9+
10+
vm.startBroadcast();
11+
12+
// Deploy the trusted message sender lookup contract
13+
deployment = deployTrustedMsgSenderLookup(deployment);
14+
15+
vm.stopBroadcast();
16+
17+
// Save the updated deployment json
18+
saveDeployment(deployment);
19+
}
20+
}

packages/coins/src/deployment/CoinsDeployerBase.sol

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {CreatorCoin} from "../CreatorCoin.sol";
1919
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
2020
import {HookUpgradeGate} from "../hooks/HookUpgradeGate.sol";
2121
import {BuySupplyWithV4SwapHook} from "../hooks/deployment/BuySupplyWithV4SwapHook.sol";
22+
import {TrustedMsgSenderProviderLookup} from "../utils/TrustedMsgSenderProviderLookup.sol";
23+
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
2224

2325
contract CoinsDeployerBase is ProxyDeployerScript {
2426
address internal constant PROTOCOL_REWARDS = 0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B;
@@ -39,6 +41,8 @@ contract CoinsDeployerBase is ProxyDeployerScript {
3941
address buySupplyWithSwapRouterHook;
4042
address zoraV4CoinHook;
4143
address hookUpgradeGate;
44+
// trusted sender lookup
45+
address trustedMsgSenderLookup;
4246
// Hook deployment salt (for deterministic deployment)
4347
bytes32 zoraV4CoinHookSalt;
4448
bool isDev;
@@ -70,6 +74,7 @@ contract CoinsDeployerBase is ProxyDeployerScript {
7074
vm.serializeAddress(objectKey, "CREATOR_COIN_IMPL", deployment.creatorCoinImpl);
7175
vm.serializeAddress(objectKey, "HOOK_UPGRADE_GATE", deployment.hookUpgradeGate);
7276
vm.serializeAddress(objectKey, "ZORA_HOOK_REGISTRY", deployment.zoraHookRegistry);
77+
vm.serializeAddress(objectKey, "TRUSTED_MSG_SENDER_LOOKUP", deployment.trustedMsgSenderLookup);
7378
string memory result = vm.serializeAddress(objectKey, "COIN_V4_IMPL", deployment.coinV4Impl);
7479

7580
vm.writeJson(result, addressesFile(deployment.isDev));
@@ -98,6 +103,7 @@ contract CoinsDeployerBase is ProxyDeployerScript {
98103
deployment.creatorCoinImpl = readAddressOrDefaultToZero(json, "CREATOR_COIN_IMPL");
99104
deployment.hookUpgradeGate = readAddressOrDefaultToZero(json, "HOOK_UPGRADE_GATE");
100105
deployment.zoraHookRegistry = readAddressOrDefaultToZero(json, "ZORA_HOOK_REGISTRY");
106+
deployment.trustedMsgSenderLookup = readAddressOrDefaultToZero(json, "TRUSTED_MSG_SENDER_LOOKUP");
101107
}
102108

103109
function deployCoinV4Impl() internal returns (ContentCoin) {
@@ -139,14 +145,23 @@ contract CoinsDeployerBase is ProxyDeployerScript {
139145
return deployment;
140146
}
141147

148+
function deployTrustedMsgSenderLookup(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
149+
// Deploy the contract directly using constructor
150+
deployment.trustedMsgSenderLookup = address(new TrustedMsgSenderProviderLookup(getDefaultTrustedMessageSenders(), getProxyAdmin()));
151+
152+
return deployment;
153+
}
154+
142155
function deployZoraV4CoinHook(CoinsDeployment memory deployment) internal returns (IHooks hook, bytes32 salt) {
156+
require(deployment.trustedMsgSenderLookup != address(0), "Trusted message sender lookup not deployed");
157+
143158
return
144159
HooksDeployment.deployHookWithExistingOrNewSalt(
145160
HooksDeployment.FOUNDRY_SCRIPT_ADDRESS,
146161
HooksDeployment.makeHookCreationCode(
147162
getUniswapV4PoolManager(),
148163
deployment.zoraFactory,
149-
getDefaultTrustedMessageSenders(),
164+
ITrustedMsgSenderProviderLookup(deployment.trustedMsgSenderLookup),
150165
deployment.hookUpgradeGate
151166
),
152167
deployment.zoraV4CoinHookSalt
@@ -175,6 +190,9 @@ contract CoinsDeployerBase is ProxyDeployerScript {
175190
function deployImpls(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
176191
// Deploy implementation contracts
177192

193+
// Deploy trusted message sender lookup first
194+
deployment = deployTrustedMsgSenderLookup(deployment);
195+
178196
// Deploy hook first, then use its address for coin v4 impl
179197
console.log("deploying content coin hook");
180198
(IHooks zoraV4CoinHook, bytes32 usedSalt) = deployZoraV4CoinHook(deployment);
@@ -191,6 +209,9 @@ contract CoinsDeployerBase is ProxyDeployerScript {
191209
}
192210

193211
function deployHooks(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
212+
// Deploy trusted message sender lookup first
213+
deployment = deployTrustedMsgSenderLookup(deployment);
214+
194215
// Deploy hook first, then use its address for coin v4 impl
195216
(IHooks zoraV4CoinHook, bytes32 usedSalt) = deployZoraV4CoinHook(deployment);
196217
deployment.zoraV4CoinHook = address(zoraV4CoinHook);

packages/coins/src/hooks/ZoraV4CoinHook.sol

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
1616
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
1717
import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
1818
import {IMsgSender} from "../interfaces/IMsgSender.sol";
19+
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
1920
import {IHasSwapPath} from "../interfaces/ICoin.sol";
2021
import {LpPosition} from "../types/LpPosition.sol";
2122
import {V4Liquidity} from "../libs/V4Liquidity.sol";
@@ -60,9 +61,10 @@ contract ZoraV4CoinHook is
6061
{
6162
using BalanceDeltaLibrary for BalanceDelta;
6263

63-
/// @notice Mapping of trusted message senders - these are addresses that are trusted to provide a
64-
/// an original msg.sender
65-
mapping(address => bool) internal trustedMessageSender;
64+
/// @dev DEPRECATED: This mapping is kept for storage compatibility. It doesn't matter that storage slots moved around
65+
/// between versions since the contracts are immutable, but in some tests we do etching to test if a new hook fixes some bugs, so we want to maintain the storage slot order.
66+
/// This slot previously held the mappings of trusted message senders.
67+
mapping(address => bool) internal legacySlot0;
6668

6769
/// @notice Mapping of pool keys to coins.
6870
mapping(bytes32 => IZoraV4CoinHook.PoolCoin) internal poolCoins;
@@ -73,27 +75,34 @@ contract ZoraV4CoinHook is
7375
/// @notice The upgrade gate contract - used to verify allowed upgrade paths
7476
IHooksUpgradeGate internal immutable upgradeGate;
7577

78+
/// @notice The trusted message sender lookup contract - used to determine if an address is trusted
79+
ITrustedMsgSenderProviderLookup internal immutable trustedMsgSenderLookup;
80+
7681
/// @notice The constructor for the ZoraV4CoinHook.
7782
/// @param poolManager_ The Uniswap V4 pool manager
7883
/// @param coinVersionLookup_ The coin version lookup contract - used to determine if an address is a coin and what version it is.
79-
/// @param trustedMessageSenders_ The addresses of the trusted message senders - these are addresses that are trusted to provide a
84+
/// @param trustedMsgSenderLookup_ The trusted message sender lookup contract - used to determine if an address is trusted
8085
/// @param upgradeGate_ The upgrade gate contract for managing hook upgrades
8186
constructor(
8287
IPoolManager poolManager_,
8388
IDeployedCoinVersionLookup coinVersionLookup_,
84-
address[] memory trustedMessageSenders_,
89+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup_,
8590
IHooksUpgradeGate upgradeGate_
8691
) BaseHook(poolManager_) {
8792
require(address(coinVersionLookup_) != address(0), CoinVersionLookupCannotBeZeroAddress());
8893

8994
require(address(upgradeGate_) != address(0), UpgradeGateCannotBeZeroAddress());
9095

96+
require(address(trustedMsgSenderLookup_) != address(0), TrustedMsgSenderLookupCannotBeZeroAddress());
97+
9198
coinVersionLookup = coinVersionLookup_;
9299
upgradeGate = upgradeGate_;
100+
trustedMsgSenderLookup = trustedMsgSenderLookup_;
101+
}
93102

94-
for (uint256 i = 0; i < trustedMessageSenders_.length; i++) {
95-
trustedMessageSender[trustedMessageSenders_[i]] = true;
96-
}
103+
/// @notice Returns the trusted message sender lookup contract
104+
function getTrustedMsgSenderLookup() external view returns (ITrustedMsgSenderProviderLookup) {
105+
return trustedMsgSenderLookup;
97106
}
98107

99108
/// @notice Returns the uniswap v4 hook settings / permissions.
@@ -120,7 +129,7 @@ contract ZoraV4CoinHook is
120129

121130
/// @inheritdoc IZoraV4CoinHook
122131
function isTrustedMessageSender(address sender) external view returns (bool) {
123-
return trustedMessageSender[sender];
132+
return trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
124133
}
125134

126135
/// @inheritdoc IZoraV4CoinHook
@@ -403,7 +412,12 @@ contract ZoraV4CoinHook is
403412
/// @return swapper The original message sender.
404413
/// @return senderIsTrusted Whether the sender is a trusted message sender.
405414
function _getOriginalMsgSender(address sender) internal view returns (address swapper, bool senderIsTrusted) {
406-
senderIsTrusted = trustedMessageSender[sender];
415+
// Always trust the zero address (0x caller)
416+
if (sender == address(0)) {
417+
senderIsTrusted = true;
418+
} else {
419+
senderIsTrusted = trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
420+
}
407421

408422
// If getter function reverts, we return a 0 address by default and continue execution.
409423
try IMsgSender(sender).msgSender() returns (address _swapper) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
2+
// This software is licensed under the Zora Delayed Open Source License.
3+
// Under this license, you may use, copy, modify, and distribute this software for
4+
// non-commercial purposes only. Commercial use and competitive products are prohibited
5+
// until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
6+
// at which point this software automatically becomes available under the MIT License.
7+
// Full license terms available at: https://docs.zora.co/coins/license
8+
pragma solidity ^0.8.23;
9+
10+
/// @title ITrustedMsgSenderProviderLookup
11+
/// @notice Interface for contracts that can determine if an address is a trusted message sender
12+
/// @dev This interface allows the hook to delegate the trusted sender check to an external contract
13+
interface ITrustedMsgSenderProviderLookup {
14+
/// @notice Checks if an address is a trusted message sender provider
15+
/// @param sender The address to check
16+
/// @return true if the sender is trusted, false otherwise
17+
function isTrustedMsgSenderProvider(address sender) external view returns (bool);
18+
}

packages/coins/src/interfaces/IZoraV4CoinHook.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ interface IZoraV4CoinHook is IUpgradeableV4Hook {
4444
/// @notice Upgrade gate cannot be the zero address.
4545
error UpgradeGateCannotBeZeroAddress();
4646

47+
/// @notice Trusted message sender lookup cannot be the zero address.
48+
error TrustedMsgSenderLookupCannotBeZeroAddress();
49+
4750
/// @notice Thrown when a pool is not initialized for the hook.
4851
/// @param key The pool key struct to identify the pool.
4952
error NoCoinForHook(PoolKey key);

packages/coins/src/libs/HooksDeployment.sol

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol";
1414
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
1515
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
1616
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
17+
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
1718

1819
Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))));
1920

@@ -86,10 +87,10 @@ library HooksDeployment {
8687
address deployer,
8788
address poolManager,
8889
address coinVersionLookup,
89-
address[] memory trustedMessageSenders,
90+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
9091
address upgradeGate
9192
) internal returns (address hookAddress, bytes32 salt) {
92-
bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
93+
bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
9394
(salt, ) = mineAndCacheSalt(deployer, hookCreationCode);
9495
hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, salt, hookCreationCode);
9596
}
@@ -131,31 +132,31 @@ library HooksDeployment {
131132
function hookConstructorArgs(
132133
address poolManager,
133134
address coinVersionLookup,
134-
address[] memory trustedMessageSenders,
135+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
135136
address upgradeGate
136137
) internal pure returns (bytes memory) {
137-
return abi.encode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
138+
return abi.encode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
138139
}
139140

140141
function makeHookCreationCode(
141142
address poolManager,
142143
address coinVersionLookup,
143-
address[] memory trustedMessageSenders,
144+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
144145
address upgradeGate
145146
) internal pure returns (bytes memory) {
146-
return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate));
147+
return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate));
147148
}
148149

149150
/// @notice Deploys or returns existing ContentCoinHook using deterministic deployment. Ensures that if a hooks is already
150151
/// deployed with the provided salt, it will be returned.
151152
function deployZoraV4CoinHook(
152153
address poolManager,
153154
address coinVersionLookup,
154-
address[] memory trustedMessageSenders,
155+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
155156
address upgradeGate,
156157
bytes32 salt
157158
) internal returns (IHooks hook) {
158-
bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
159+
bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
159160
return deployHookWithSalt(creationCode, salt);
160161
}
161162

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
2+
// This software is licensed under the Zora Delayed Open Source License.
3+
// Under this license, you may use, copy, modify, and distribute this software for
4+
// non-commercial purposes only. Commercial use and competitive products are prohibited
5+
// until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
6+
// at which point this software automatically becomes available under the MIT License.
7+
// Full license terms available at: https://docs.zora.co/coins/license
8+
pragma solidity ^0.8.23;
9+
10+
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
11+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
12+
import {ContractVersionBase} from "../version/ContractVersionBase.sol";
13+
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
14+
15+
/// @title TrustedMsgSenderProviderLookup
16+
/// @notice Contract for ITrustedMsgSenderProviderLookup that manages trusted message senders
17+
/// @dev This contract allows the owner to add/remove trusted senders and provides lookup functionality
18+
contract TrustedMsgSenderProviderLookup is ITrustedMsgSenderProviderLookup, ContractVersionBase, Ownable2Step {
19+
/// @notice Emitted when a trusted sender is added
20+
/// @param sender The address that was added as trusted
21+
event TrustedSenderAdded(address indexed sender);
22+
23+
/// @notice Emitted when a trusted sender is removed
24+
/// @param sender The address that was removed from trusted
25+
event TrustedSenderRemoved(address indexed sender);
26+
27+
/// @notice Mapping of addresses to their trusted sender status
28+
mapping(address => bool) private trustedSenders;
29+
30+
/// @notice Constructor that initializes the contract with trusted senders and sets the owner
31+
/// @param trustedMessageSenders Array of addresses to mark as trusted senders initially
32+
/// @param initialOwner The address that will own this contract
33+
constructor(address[] memory trustedMessageSenders, address initialOwner) Ownable(initialOwner) {
34+
for (uint256 i = 0; i < trustedMessageSenders.length; i++) {
35+
trustedSenders[trustedMessageSenders[i]] = true;
36+
emit TrustedSenderAdded(trustedMessageSenders[i]);
37+
}
38+
}
39+
40+
/// @notice Checks if an address is a trusted message sender provider
41+
/// @param sender The address to check
42+
/// @return true if the sender is trusted, false otherwise
43+
function isTrustedMsgSenderProvider(address sender) external view override returns (bool) {
44+
return trustedSenders[sender];
45+
}
46+
47+
/// @notice Adds multiple trusted senders in a single transaction (only callable by owner)
48+
/// @param senders Array of addresses to add as trusted
49+
function addTrustedMsgSenderProviders(address[] calldata senders) external onlyOwner {
50+
for (uint256 i = 0; i < senders.length; i++) {
51+
address sender = senders[i];
52+
require(sender != address(0), "Cannot add zero address as trusted sender");
53+
54+
if (!trustedSenders[sender]) {
55+
trustedSenders[sender] = true;
56+
emit TrustedSenderAdded(sender);
57+
}
58+
}
59+
}
60+
61+
/// @notice Removes multiple trusted senders in a single transaction (only callable by owner)
62+
/// @param senders Array of addresses to remove from trusted
63+
function removeTrustedMsgSenderProviders(address[] calldata senders) external onlyOwner {
64+
for (uint256 i = 0; i < senders.length; i++) {
65+
address sender = senders[i];
66+
67+
if (trustedSenders[sender]) {
68+
trustedSenders[sender] = false;
69+
emit TrustedSenderRemoved(sender);
70+
}
71+
}
72+
}
73+
}

packages/coins/test/CreatorCoinRewards.t.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,16 @@ contract CreatorCoinRewardsTest is BaseTest {
289289
function test_buy_then_sell_both_referrers() public {
290290
uint128 buyAmount = 100 ether; // Fixed amount
291291

292+
console.log("deploying creator coin with platform referrer");
292293
// Deploy CreatorCoin with platform referrer
293294
_deployCreatorCoin(true);
294295

296+
console.log("buying creator coin with both referrers");
295297
// Step 1: Buy creator coin (ZORA -> Creator Coin)
296298
_buyCreatorCoin(buyAmount, true);
297299

300+
console.log("buyer's creator coin balance", creatorCoin.balanceOf(users.buyer));
301+
298302
// Get buyer's creator coin balance after purchase
299303
uint256 creatorCoinBalance = creatorCoin.balanceOf(users.buyer);
300304
require(creatorCoinBalance > 0, "Buyer must have creator coin balance to sell");

0 commit comments

Comments
 (0)