@zoralabs/coins 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.abi-stability +923 -0
- package/.turbo/turbo-build$colon$js.log +110 -116
- package/CHANGELOG.md +25 -0
- package/abis/Address.json +0 -16
- package/abis/BaseCoin.json +18 -0
- package/abis/BuySupplyWithSwapRouterHook.json +0 -27
- package/abis/BuySupplyWithV4SwapHook.json +0 -32
- package/abis/Clones.json +1 -1
- package/abis/CoinDopplerMultiCurve.json +109 -0
- package/abis/ContentCoin.json +18 -0
- package/abis/Create2.json +0 -21
- package/abis/CreatorCoin.json +18 -0
- package/abis/ERC1967Proxy.json +1 -1
- package/abis/ERC1967Utils.json +0 -45
- package/abis/{UpgradeCoinImpl.json → Errors.json} +14 -10
- package/abis/{MockERC20.json → IERC1363.json} +134 -104
- package/abis/IERC1967.json +47 -0
- package/abis/IERC20.json +0 -36
- package/abis/IHasCreationInfo.json +20 -0
- package/abis/IProtocolRewards.json +0 -258
- package/abis/{Script.json → ISupportsLimitOrderFill.json} +2 -2
- package/abis/IZoraLimitOrderBookCoinsInterface.json +67 -0
- package/abis/IZoraV4CoinHook.json +10 -0
- package/abis/ProxyShim.json +15 -16
- package/abis/SafeCast.json +51 -0
- package/abis/{AddressConstants.json → SafeCast160.json} +1 -1
- package/abis/Strings.json +10 -0
- package/abis/UUPSUpgradeable.json +1 -1
- package/abis/V3ToV4SwapLib.json +28 -0
- package/abis/ZoraFactory.json +1 -1
- package/abis/ZoraFactoryImpl.json +22 -6
- package/abis/ZoraV4CoinHook.json +20 -48
- package/dist/index.cjs +980 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +978 -41
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +1501 -76
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/package/wagmiGenerated.ts +981 -44
- package/package.json +11 -9
- package/remappings.txt +2 -1
- package/src/BaseCoin.sol +32 -2
- package/src/ZoraFactoryImpl.sol +8 -0
- package/src/deployment/ForkedCoinsAddresses.sol +54 -0
- package/src/hooks/ZoraV4CoinHook.sol +131 -20
- package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +20 -142
- package/src/interfaces/IHasCreationInfo.sol +12 -0
- package/src/interfaces/ISupportsLimitOrderFill.sol +11 -0
- package/src/interfaces/IZoraLimitOrderBookCoinsInterface.sol +21 -0
- package/src/interfaces/IZoraV4CoinHook.sol +6 -0
- package/src/libs/CoinConstants.sol +22 -0
- package/src/libs/CoinDopplerMultiCurve.sol +1 -1
- package/src/libs/CoinRewardsV4.sol +0 -1
- package/src/libs/CoinSetup.sol +7 -1
- package/src/libs/HooksDeployment.sol +20 -8
- package/src/libs/UniV4SwapHelper.sol +35 -0
- package/src/libs/V3ToV4SwapLib.sol +265 -0
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/BuySupplyWithV4SwapHook.t.sol +4 -3
- package/test/Coin.t.sol +7 -1
- package/test/CoinUniV4.t.sol +6 -1
- package/test/ContentCoinRewards.t.sol +6 -1
- package/test/CreatorCoin.t.sol +3 -1
- package/test/CreatorCoinRewards.t.sol +4 -1
- package/test/Factory.t.sol +20 -7
- package/test/HooksDeployment.t.sol +16 -3
- package/test/LaunchFee.t.sol +286 -0
- package/test/LiquidityMigration.t.sol +52 -44
- package/test/MultiOwnable.t.sol +2 -1
- package/test/Upgrades.t.sol +110 -81
- package/test/V4Liquidity.t.sol +1 -1
- package/test/mocks/MockSwapRouter.sol +33 -0
- package/test/mocks/MockZoraLimitOrderBook.sol +14 -0
- package/test/utils/BaseTest.sol +14 -448
- package/test/utils/FeeEstimatorHook.sol +6 -2
- package/test/utils/V4TestSetup.sol +595 -0
- package/wagmi.config.ts +1 -1
- package/abis/BaseTest.json +0 -718
- package/abis/DeterministicDeployerAndCaller.json +0 -315
- package/abis/DeterministicUUPSProxyDeployer.json +0 -167
- package/abis/EIP712.json +0 -67
- package/abis/ERC20.json +0 -310
- package/abis/FeeEstimatorHook.json +0 -1938
- package/abis/IERC721.json +0 -287
- package/abis/IERC721Enumerable.json +0 -343
- package/abis/IERC721Metadata.json +0 -332
- package/abis/IERC721TokenReceiver.json +0 -36
- package/abis/IImmutableCreate2Factory.json +0 -93
- package/abis/IMulticall3.json +0 -440
- package/abis/ISafe.json +0 -15
- package/abis/ISymbol.json +0 -15
- package/abis/IUniswapV4Router04.json +0 -484
- package/abis/IUniversalRouter.json +0 -61
- package/abis/IV4Quoter.json +0 -310
- package/abis/ImmutableCreate2FactoryUtils.json +0 -15
- package/abis/LibString.json +0 -7
- package/abis/Math.json +0 -7
- package/abis/MockAirlock.json +0 -39
- package/abis/MockERC721.json +0 -350
- package/abis/ProtocolRewards.json +0 -494
- package/abis/ShortStrings.json +0 -18
- package/abis/SimpleERC20.json +0 -326
- package/abis/StdAssertions.json +0 -379
- package/abis/StdInvariant.json +0 -180
- package/abis/Test.json +0 -570
- package/abis/VmContractHelper235.json +0 -233
- package/abis/VmContractHelper242.json +0 -233
- package/abis/stdError.json +0 -119
- package/abis/stdStorageSafe.json +0 -52
- package/addresses/8453.json +0 -13
- package/addresses/84532.json +0 -10
- package/deterministicConfig/deployerAndCaller.json +0 -5
- package/deterministicConfig/zoraFactory.json +0 -8
- package/script/Deploy.s.sol +0 -23
- package/script/DeployAutoSwapper.s.sol +0 -30
- package/script/DeployDevFactory.s.sol +0 -21
- package/script/DeployPostDeploymentHooks.s.sol +0 -20
- package/script/DeployTrustedMsgSenderLookup.s.sol +0 -20
- package/script/DeployUpgradeGate.s.sol +0 -21
- package/script/GenerateDeterministicParams.s.sol +0 -43
- package/script/PrintRegisterUpgradePath.s.sol +0 -28
- package/script/PrintUpgradeCommand.s.sol +0 -13
- package/script/TestBackingCoinSwap.s.sol +0 -144
- package/script/TestV4Swap.s.sol +0 -133
- package/script/UpgradeCoinImpl.sol +0 -23
- package/script/UpgradeFactoryImpl.s.sol +0 -28
- package/script/UpgradeHooks.s.sol +0 -23
- package/src/deployment/CoinsDeployerBase.sol +0 -297
- /package/{test → src}/utils/ProxyShim.sol +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoralabs/coins",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@openzeppelin/contracts": "
|
|
18
|
-
"@openzeppelin/contracts-upgradeable": "
|
|
17
|
+
"@openzeppelin/contracts": "5.4.0",
|
|
18
|
+
"@openzeppelin/contracts-upgradeable": "5.4.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^20.1.2",
|
|
@@ -35,20 +35,22 @@
|
|
|
35
35
|
"tsx": "^3.13.0",
|
|
36
36
|
"typescript": "^5.2.2",
|
|
37
37
|
"viem": "^2.21.18",
|
|
38
|
+
"@zoralabs/shared-contracts": "^0.0.5",
|
|
38
39
|
"@zoralabs/shared-scripts": "^0.0.0",
|
|
39
|
-
"@zoralabs/tsconfig": "^0.0.1"
|
|
40
|
-
"@zoralabs/shared-contracts": "^0.0.5"
|
|
40
|
+
"@zoralabs/tsconfig": "^0.0.1"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "forge build",
|
|
44
|
-
"build:contracts:minimal": "forge build
|
|
44
|
+
"build:contracts:minimal": "forge build src/ --no-metadata",
|
|
45
45
|
"build:js": "pnpm run wagmi:generate && pnpm run copy-abis && pnpm run prettier:write && tsup",
|
|
46
46
|
"build:sizes": "forge build src/ --sizes",
|
|
47
47
|
"copy-abis": "pnpm exec bundle-abis",
|
|
48
|
-
"coverage": "forge coverage --report lcov --ir-minimum --no-match-coverage '(test/|src/utils/uniswap/|
|
|
49
|
-
"prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol'
|
|
50
|
-
"prettier:write": "prettier --write 'src/**/*.sol' 'test/**/*.sol'
|
|
48
|
+
"coverage": "forge coverage --report lcov --ir-minimum --no-match-coverage '(test/|src/utils/uniswap/|src/deployment/)'",
|
|
49
|
+
"prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol'",
|
|
50
|
+
"prettier:write": "prettier --write 'src/**/*.sol' 'test/**/*.sol'",
|
|
51
51
|
"test": "forge test -vv",
|
|
52
|
+
"abi-check:check": "../../scripts/abi-check.sh check",
|
|
53
|
+
"abi-check:generate": "../../scripts/abi-check.sh generate",
|
|
52
54
|
"test-gas": "forge test --gas-report",
|
|
53
55
|
"update-contract-version": "pnpm exec update-contract-version",
|
|
54
56
|
"wagmi:generate": "pnpm run build:contracts:minimal && wagmi generate && pnpm exec rename-generated-abi-casing ./package/wagmiGenerated.ts"
|
package/remappings.txt
CHANGED
|
@@ -2,7 +2,8 @@ ds-test/=node_modules/ds-test/src/
|
|
|
2
2
|
forge-std/=node_modules/forge-std/src/
|
|
3
3
|
@openzeppelin/=node_modules/@openzeppelin/
|
|
4
4
|
@zoralabs/shared-contracts/=node_modules/@zoralabs/shared-contracts/src/
|
|
5
|
-
|
|
5
|
+
@zoralabs/limit-orders/=node_modules/@zoralabs/limit-orders/
|
|
6
|
+
solady/=node_modules/solady/src/
|
|
6
7
|
@uniswap/v4-core/=node_modules/@uniswap/v4-core/
|
|
7
8
|
@uniswap/v4-periphery/=node_modules/@uniswap/v4-periphery/
|
|
8
9
|
permit2/src/=node_modules/@uniswap/permit2/src/
|
package/src/BaseCoin.sol
CHANGED
|
@@ -11,6 +11,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
11
11
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
12
12
|
import {ICoin, IHasTotalSupplyForPositions, IHasCoinType} from "./interfaces/ICoin.sol";
|
|
13
13
|
import {IHasRewardsRecipients} from "./interfaces/IHasRewardsRecipients.sol";
|
|
14
|
+
import {IHasCreationInfo} from "./interfaces/IHasCreationInfo.sol";
|
|
14
15
|
import {ICoinComments} from "./interfaces/ICoinComments.sol";
|
|
15
16
|
import {IERC7572} from "./interfaces/IERC7572.sol";
|
|
16
17
|
import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
|
|
@@ -52,7 +53,7 @@ import {PoolState} from "./types/PoolState.sol";
|
|
|
52
53
|
\$$$$$$ | $$$$$$ |$$$$$$\ $$ | \$$ |
|
|
53
54
|
\______/ \______/ \______|\__| \__|
|
|
54
55
|
*/
|
|
55
|
-
abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ERC165Upgradeable {
|
|
56
|
+
abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ERC165Upgradeable {
|
|
56
57
|
using SafeERC20 for IERC20;
|
|
57
58
|
|
|
58
59
|
/// @notice The address of the protocol rewards contract
|
|
@@ -84,6 +85,11 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
|
|
|
84
85
|
string private _name;
|
|
85
86
|
/// @notice The symbol of the token
|
|
86
87
|
string private _symbol;
|
|
88
|
+
/// @notice The timestamp when the coin was created
|
|
89
|
+
uint256 private _creationTimestamp;
|
|
90
|
+
|
|
91
|
+
/// @dev Transient storage slot for tracking deployment state
|
|
92
|
+
bytes32 private constant _IS_DEPLOYING_SLOT = keccak256("BaseCoin.isDeploying");
|
|
87
93
|
|
|
88
94
|
/**
|
|
89
95
|
* @notice The constructor for the static Coin contract deployment shared across all Coins.
|
|
@@ -125,6 +131,12 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
|
|
|
125
131
|
uint160 sqrtPriceX96,
|
|
126
132
|
PoolConfiguration memory poolConfiguration_
|
|
127
133
|
) public virtual initializer {
|
|
134
|
+
// Set transient deploying flag for launch fee bypass
|
|
135
|
+
_setIsDeploying(true);
|
|
136
|
+
|
|
137
|
+
// Record creation timestamp for launch fee calculation
|
|
138
|
+
_creationTimestamp = block.timestamp;
|
|
139
|
+
|
|
128
140
|
currency = currency_;
|
|
129
141
|
// we need to set this before initialization, because
|
|
130
142
|
// distributing currency relies on the poolkey being set since the hooks
|
|
@@ -253,7 +265,25 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
|
|
|
253
265
|
interfaceId == type(IHasPoolKey).interfaceId ||
|
|
254
266
|
interfaceId == type(IHasCoinType).interfaceId ||
|
|
255
267
|
interfaceId == type(IHasTotalSupplyForPositions).interfaceId ||
|
|
256
|
-
interfaceId == type(IHasSwapPath).interfaceId
|
|
268
|
+
interfaceId == type(IHasSwapPath).interfaceId ||
|
|
269
|
+
interfaceId == type(IHasCreationInfo).interfaceId;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// @inheritdoc IHasCreationInfo
|
|
273
|
+
function creationInfo() external view returns (uint256 creationTimestamp, bool isDeploying) {
|
|
274
|
+
creationTimestamp = _creationTimestamp;
|
|
275
|
+
bytes32 slot = _IS_DEPLOYING_SLOT;
|
|
276
|
+
assembly {
|
|
277
|
+
isDeploying := tload(slot)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// @dev Sets the transient deploying flag
|
|
282
|
+
function _setIsDeploying(bool value) internal {
|
|
283
|
+
bytes32 slot = _IS_DEPLOYING_SLOT;
|
|
284
|
+
assembly {
|
|
285
|
+
tstore(slot, value)
|
|
286
|
+
}
|
|
257
287
|
}
|
|
258
288
|
|
|
259
289
|
/// @dev Overrides ERC20's _update function to emit a superset `CoinTransfer` event
|
package/src/ZoraFactoryImpl.sol
CHANGED
|
@@ -390,6 +390,14 @@ contract ZoraFactoryImpl is
|
|
|
390
390
|
__UUPSUpgradeable_init();
|
|
391
391
|
__ReentrancyGuard_init();
|
|
392
392
|
__Ownable_init(initialOwner);
|
|
393
|
+
|
|
394
|
+
address[] memory hooks = new address[](1);
|
|
395
|
+
string[] memory tags = new string[](1);
|
|
396
|
+
|
|
397
|
+
hooks[0] = hook;
|
|
398
|
+
tags[0] = "CoinHook";
|
|
399
|
+
|
|
400
|
+
IZoraHookRegistry(zoraHookRegistry).registerHooks(hooks, tags);
|
|
393
401
|
}
|
|
394
402
|
|
|
395
403
|
/// @notice The implementation address of the factory contract
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/// @notice Minimal deployment base for coins tests
|
|
5
|
+
/// @dev Provides hardcoded deployment addresses for testing
|
|
6
|
+
contract ForkedCoinsAddresses {
|
|
7
|
+
struct CoinsDeployment {
|
|
8
|
+
// Factory
|
|
9
|
+
address zoraFactory;
|
|
10
|
+
address zoraFactoryImpl;
|
|
11
|
+
// Implementation
|
|
12
|
+
address coinV3Impl;
|
|
13
|
+
address coinV4Impl;
|
|
14
|
+
address creatorCoinImpl;
|
|
15
|
+
string coinVersion;
|
|
16
|
+
// hooks
|
|
17
|
+
address buySupplyWithSwapRouterHook;
|
|
18
|
+
address zoraV4CoinHook;
|
|
19
|
+
address hookUpgradeGate;
|
|
20
|
+
// trusted sender lookup
|
|
21
|
+
address trustedMsgSenderLookup;
|
|
22
|
+
// Hook deployment salt (for deterministic deployment)
|
|
23
|
+
bytes32 zoraV4CoinHookSalt;
|
|
24
|
+
bool isDev;
|
|
25
|
+
// Hook registry
|
|
26
|
+
address zoraHookRegistry;
|
|
27
|
+
// Limit order book
|
|
28
|
+
address zoraLimitOrderBook;
|
|
29
|
+
address swapWithLimitOrdersRouter;
|
|
30
|
+
address orderBookAuthority;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
address internal constant ZORA = 0x1111111111166b7FE7bd91427724B487980aFc69;
|
|
34
|
+
|
|
35
|
+
function readDeployment() internal pure returns (CoinsDeployment memory deployment) {
|
|
36
|
+
return readDeployment(false);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readDeployment(bool dev) internal pure returns (CoinsDeployment memory deployment) {
|
|
40
|
+
// Hardcoded Base mainnet deployment addresses
|
|
41
|
+
deployment.zoraFactory = 0x777777751622c0d3258f214F9DF38E35BF45baF3;
|
|
42
|
+
deployment.zoraFactoryImpl = 0x8Ec7f068A77fa5FC1925110f82381374BA054Ff2;
|
|
43
|
+
deployment.coinV3Impl = 0x45Bf86430af7CD071Ea23aE52325A78C8d12aD5a;
|
|
44
|
+
deployment.coinV4Impl = 0x7Cad62748DDf516CF85bC2C05C14786D84Cf861c;
|
|
45
|
+
deployment.creatorCoinImpl = 0x36853f9f48fAEe51Bd3db15db21EB4B9038bB795;
|
|
46
|
+
deployment.coinVersion = "2.3.0";
|
|
47
|
+
deployment.buySupplyWithSwapRouterHook = 0xd8CC7bCA1dE52eA788829B16E375e9B96C18D433;
|
|
48
|
+
deployment.zoraV4CoinHook = 0xC8d077444625eB300A427a6dfB2b1DBf9b159040;
|
|
49
|
+
deployment.hookUpgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2;
|
|
50
|
+
deployment.zoraV4CoinHookSalt = 0x0000000000000000000000000000000000000000000000000000000000001624;
|
|
51
|
+
deployment.zoraHookRegistry = 0x777777C4c14b133858c3982D41Dbf02509fc18d7;
|
|
52
|
+
deployment.isDev = dev;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -5,41 +5,44 @@
|
|
|
5
5
|
// until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
|
|
6
6
|
// at which point this software automatically becomes available under the MIT License.
|
|
7
7
|
// Full license terms available at: https://docs.zora.co/coins/license
|
|
8
|
-
pragma solidity ^0.8.
|
|
8
|
+
pragma solidity ^0.8.28;
|
|
9
9
|
|
|
10
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
11
|
+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
|
10
12
|
import {BaseHook} from "@uniswap/v4-periphery/src/utils/BaseHook.sol";
|
|
11
13
|
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
|
|
12
|
-
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
13
14
|
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
14
15
|
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
15
16
|
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
16
17
|
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
18
|
+
import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
|
|
19
|
+
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
20
|
+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
21
|
+
import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol";
|
|
22
|
+
|
|
17
23
|
import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
|
|
18
24
|
import {IMsgSender} from "../interfaces/IMsgSender.sol";
|
|
19
25
|
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
|
|
20
|
-
import {IHasSwapPath} from "../interfaces/ICoin.sol";
|
|
26
|
+
import {ICoin, IHasSwapPath, IHasRewardsRecipients, IHasCoinType} from "../interfaces/ICoin.sol";
|
|
27
|
+
import {IHasCreationInfo} from "../interfaces/IHasCreationInfo.sol";
|
|
28
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
29
|
+
import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
|
|
30
|
+
import {IUpgradeableV4Hook, IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee, BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
|
|
31
|
+
import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
|
|
32
|
+
import {IZoraHookRegistry} from "../interfaces/IZoraHookRegistry.sol";
|
|
33
|
+
import {IZoraLimitOrderBookCoinsInterface} from "../interfaces/IZoraLimitOrderBookCoinsInterface.sol";
|
|
21
34
|
import {LpPosition} from "../types/LpPosition.sol";
|
|
22
35
|
import {V4Liquidity} from "../libs/V4Liquidity.sol";
|
|
23
36
|
import {CoinRewardsV4} from "../libs/CoinRewardsV4.sol";
|
|
24
|
-
import {ICoin} from "../interfaces/ICoin.sol";
|
|
25
|
-
import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
|
|
26
37
|
import {CoinCommon} from "../libs/CoinCommon.sol";
|
|
27
38
|
import {CoinDopplerMultiCurve} from "../libs/CoinDopplerMultiCurve.sol";
|
|
28
39
|
import {PoolStateReader} from "../libs/PoolStateReader.sol";
|
|
29
|
-
import {IHasRewardsRecipients} from "../interfaces/ICoin.sol";
|
|
30
40
|
import {CoinConfigurationVersions} from "../libs/CoinConfigurationVersions.sol";
|
|
31
|
-
import {
|
|
32
|
-
import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
|
|
33
|
-
import {MultiOwnable} from "../utils/MultiOwnable.sol";
|
|
34
|
-
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
35
|
-
import {IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee} from "../interfaces/IUpgradeableV4Hook.sol";
|
|
36
|
-
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
|
37
|
-
import {BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
|
|
41
|
+
import {CoinConstants} from "../libs/CoinConstants.sol";
|
|
38
42
|
import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
|
|
39
43
|
import {TickMath} from "../utils/uniswap/TickMath.sol";
|
|
40
44
|
import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
|
|
41
|
-
import {
|
|
42
|
-
import {CoinConstants} from "../libs/CoinConstants.sol";
|
|
45
|
+
import {ISupportsLimitOrderFill} from "../interfaces/ISupportsLimitOrderFill.sol";
|
|
43
46
|
|
|
44
47
|
/// @title ZoraV4CoinHook
|
|
45
48
|
/// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
|
|
@@ -78,26 +81,38 @@ contract ZoraV4CoinHook is
|
|
|
78
81
|
/// @notice The trusted message sender lookup contract - used to determine if an address is trusted
|
|
79
82
|
ITrustedMsgSenderProviderLookup internal immutable trustedMsgSenderLookup;
|
|
80
83
|
|
|
84
|
+
/// @notice The Zora limit order book contract - used to fill limit orders during swaps
|
|
85
|
+
IZoraLimitOrderBookCoinsInterface internal immutable zoraLimitOrderBook;
|
|
86
|
+
|
|
87
|
+
/// @notice The Zora hook registry
|
|
88
|
+
IZoraHookRegistry internal immutable zoraHookRegistry;
|
|
89
|
+
|
|
81
90
|
/// @notice The constructor for the ZoraV4CoinHook.
|
|
82
91
|
/// @param poolManager_ The Uniswap V4 pool manager
|
|
83
92
|
/// @param coinVersionLookup_ The coin version lookup contract - used to determine if an address is a coin and what version it is.
|
|
84
93
|
/// @param trustedMsgSenderLookup_ The trusted message sender lookup contract - used to determine if an address is trusted
|
|
85
94
|
/// @param upgradeGate_ The upgrade gate contract for managing hook upgrades
|
|
95
|
+
/// @param zoraLimitOrderBook_ The Zora limit order book contract for filling orders during swaps
|
|
96
|
+
/// @param zoraHookRegistry_ The Zora hook registry contract for identifying registered hooks
|
|
86
97
|
constructor(
|
|
87
98
|
IPoolManager poolManager_,
|
|
88
99
|
IDeployedCoinVersionLookup coinVersionLookup_,
|
|
89
100
|
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup_,
|
|
90
|
-
IHooksUpgradeGate upgradeGate_
|
|
101
|
+
IHooksUpgradeGate upgradeGate_,
|
|
102
|
+
IZoraLimitOrderBookCoinsInterface zoraLimitOrderBook_,
|
|
103
|
+
IZoraHookRegistry zoraHookRegistry_
|
|
91
104
|
) BaseHook(poolManager_) {
|
|
92
105
|
require(address(coinVersionLookup_) != address(0), CoinVersionLookupCannotBeZeroAddress());
|
|
93
|
-
|
|
94
106
|
require(address(upgradeGate_) != address(0), UpgradeGateCannotBeZeroAddress());
|
|
95
|
-
|
|
107
|
+
require(address(zoraLimitOrderBook_) != address(0), ZoraLimitOrderBookCannotBeZeroAddress());
|
|
108
|
+
require(address(zoraHookRegistry_) != address(0), ZoraHookRegistryCannotBeZeroAddress());
|
|
96
109
|
require(address(trustedMsgSenderLookup_) != address(0), TrustedMsgSenderLookupCannotBeZeroAddress());
|
|
97
110
|
|
|
98
111
|
coinVersionLookup = coinVersionLookup_;
|
|
99
112
|
upgradeGate = upgradeGate_;
|
|
100
113
|
trustedMsgSenderLookup = trustedMsgSenderLookup_;
|
|
114
|
+
zoraLimitOrderBook = zoraLimitOrderBook_;
|
|
115
|
+
zoraHookRegistry = zoraHookRegistry_;
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
/// @notice Returns the trusted message sender lookup contract
|
|
@@ -116,7 +131,7 @@ contract ZoraV4CoinHook is
|
|
|
116
131
|
afterAddLiquidity: false,
|
|
117
132
|
beforeRemoveLiquidity: false,
|
|
118
133
|
afterRemoveLiquidity: false,
|
|
119
|
-
beforeSwap:
|
|
134
|
+
beforeSwap: true,
|
|
120
135
|
afterSwap: true,
|
|
121
136
|
beforeDonate: false,
|
|
122
137
|
afterDonate: false,
|
|
@@ -148,7 +163,8 @@ contract ZoraV4CoinHook is
|
|
|
148
163
|
super.supportsInterface(interfaceId) ||
|
|
149
164
|
interfaceId == type(IUpgradeableDestinationV4Hook).interfaceId ||
|
|
150
165
|
interfaceId == type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId ||
|
|
151
|
-
interfaceId == type(IVersionedContract).interfaceId
|
|
166
|
+
interfaceId == type(IVersionedContract).interfaceId ||
|
|
167
|
+
interfaceId == type(ISupportsLimitOrderFill).interfaceId;
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
/// @notice Internal fn generating the positions for a given pool key.
|
|
@@ -281,6 +297,77 @@ contract ZoraV4CoinHook is
|
|
|
281
297
|
V4Liquidity.lockAndMint(poolManager, key, positions);
|
|
282
298
|
}
|
|
283
299
|
|
|
300
|
+
/// @notice Transiently stores the tick before a swap and calculates the launch fee.
|
|
301
|
+
/// @dev This is used in `_afterSwap` to determine the ticks crossed during the swap.
|
|
302
|
+
/// Also returns a dynamic fee that decays from 99% to 1% over 10 seconds after coin creation.
|
|
303
|
+
function _beforeSwap(
|
|
304
|
+
address sender,
|
|
305
|
+
PoolKey calldata key,
|
|
306
|
+
SwapParams calldata,
|
|
307
|
+
bytes calldata
|
|
308
|
+
) internal virtual override returns (bytes4, BeforeSwapDelta, uint24) {
|
|
309
|
+
if (_isInternalSwap(sender)) {
|
|
310
|
+
return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Store tick for user-initiated swaps only
|
|
314
|
+
(, int24 currentTick, , ) = StateLibrary.getSlot0(poolManager, key.toId());
|
|
315
|
+
|
|
316
|
+
TransientSlot.Int256Slot slot = TransientSlot.asInt256(CoinConstants._BEFORE_SWAP_TICK_SLOT);
|
|
317
|
+
TransientSlot.tstore(slot, int256(currentTick));
|
|
318
|
+
|
|
319
|
+
// Calculate launch fee
|
|
320
|
+
bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
|
|
321
|
+
address coin = poolCoins[poolKeyHash].coin;
|
|
322
|
+
uint24 fee = _calculateLaunchFee(coin);
|
|
323
|
+
|
|
324
|
+
return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), fee);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// @notice Calculates the launch fee based on coin creation time
|
|
328
|
+
/// @dev Returns fee with OVERRIDE_FEE_FLAG to signal V4 to use this fee.
|
|
329
|
+
/// Fee decays linearly from LAUNCH_FEE_START (99%) to LP_FEE_V4 (1%) over LAUNCH_FEE_DURATION.
|
|
330
|
+
/// Returns LP_FEE_V4 for legacy coins that don't support IHasCreationInfo.
|
|
331
|
+
/// Bypasses launch fee (returns LP_FEE_V4) during initial coin deployment.
|
|
332
|
+
/// @param coin The coin address
|
|
333
|
+
/// @return fee The calculated fee with OVERRIDE_FEE_FLAG
|
|
334
|
+
function _calculateLaunchFee(address coin) internal view returns (uint24 fee) {
|
|
335
|
+
// Check if coin supports creation info interface (legacy coins won't)
|
|
336
|
+
try IERC165(coin).supportsInterface(type(IHasCreationInfo).interfaceId) returns (bool supported) {
|
|
337
|
+
if (!supported) {
|
|
338
|
+
// Legacy coin - use normal LP fee
|
|
339
|
+
return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
// supportsInterface call failed - use normal LP fee
|
|
343
|
+
return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Get creation info from coin
|
|
347
|
+
(uint256 creationTimestamp, bool isDeploying) = IHasCreationInfo(coin).creationInfo();
|
|
348
|
+
|
|
349
|
+
// Bypass launch fee during initial deployment
|
|
350
|
+
if (isDeploying) {
|
|
351
|
+
return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Calculate elapsed time since creation
|
|
355
|
+
uint256 elapsed = block.timestamp - creationTimestamp;
|
|
356
|
+
|
|
357
|
+
// If launch fee duration has passed, use normal LP fee
|
|
358
|
+
if (elapsed >= CoinConstants.LAUNCH_FEE_DURATION) {
|
|
359
|
+
return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Linear decay: fee = startFee - (elapsed / duration) * (startFee - endFee)
|
|
363
|
+
uint256 startFee = CoinConstants.LAUNCH_FEE_START;
|
|
364
|
+
uint256 endFee = CoinConstants.LP_FEE_V4;
|
|
365
|
+
uint256 feeReduction = (elapsed * (startFee - endFee)) / CoinConstants.LAUNCH_FEE_DURATION;
|
|
366
|
+
fee = uint24(startFee - feeReduction);
|
|
367
|
+
|
|
368
|
+
return CoinConstants.OVERRIDE_FEE_FLAG | fee;
|
|
369
|
+
}
|
|
370
|
+
|
|
284
371
|
/// @notice Internal fn called when a swap is executed.
|
|
285
372
|
/// @dev This hook is called from BaseHook library from uniswap v4.
|
|
286
373
|
/// This hook:
|
|
@@ -301,6 +388,10 @@ contract ZoraV4CoinHook is
|
|
|
301
388
|
BalanceDelta delta,
|
|
302
389
|
bytes calldata hookData
|
|
303
390
|
) internal virtual override returns (bytes4, int128) {
|
|
391
|
+
if (_isInternalSwap(sender)) {
|
|
392
|
+
return (BaseHook.afterSwap.selector, 0);
|
|
393
|
+
}
|
|
394
|
+
|
|
304
395
|
bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
|
|
305
396
|
|
|
306
397
|
// get the coin address and positions for the pool key; they must have been set in the afterInitialize callback
|
|
@@ -343,9 +434,24 @@ contract ZoraV4CoinHook is
|
|
|
343
434
|
);
|
|
344
435
|
}
|
|
345
436
|
|
|
437
|
+
(int24 tickBeforeSwap, int24 tickAfterSwap) = _getSwapTickRange(key);
|
|
438
|
+
|
|
439
|
+
// Derive fill direction from actual tick movement
|
|
440
|
+
if (tickAfterSwap != tickBeforeSwap) {
|
|
441
|
+
bool isCurrency0 = tickAfterSwap > tickBeforeSwap;
|
|
442
|
+
zoraLimitOrderBook.fill(key, isCurrency0, tickBeforeSwap, tickAfterSwap, CoinConstants.SENTINEL_DEFAULT_LIMIT_ORDER_FILL_COUNT, address(0));
|
|
443
|
+
}
|
|
444
|
+
|
|
346
445
|
return (BaseHook.afterSwap.selector, 0);
|
|
347
446
|
}
|
|
348
447
|
|
|
448
|
+
/// @dev Get the tick range of a swap
|
|
449
|
+
function _getSwapTickRange(PoolKey calldata key) internal view returns (int24 tickBeforeSwap, int24 tickAfterSwap) {
|
|
450
|
+
TransientSlot.Int256Slot slot = TransientSlot.asInt256(CoinConstants._BEFORE_SWAP_TICK_SLOT);
|
|
451
|
+
tickBeforeSwap = int24(int256(TransientSlot.tload(slot)));
|
|
452
|
+
(, tickAfterSwap, , ) = StateLibrary.getSlot0(poolManager, key.toId());
|
|
453
|
+
}
|
|
454
|
+
|
|
349
455
|
/// @dev Internal fn to allow for overriding market reward distribution logic
|
|
350
456
|
function _distributeMarketRewards(Currency currency, uint128 fees, IHasRewardsRecipients coin, address tradeReferrer) internal virtual {
|
|
351
457
|
// get rewards distribution methodology from the coin
|
|
@@ -395,6 +501,11 @@ contract ZoraV4CoinHook is
|
|
|
395
501
|
delete poolCoins[poolKeyHash];
|
|
396
502
|
}
|
|
397
503
|
|
|
504
|
+
/// @dev Checks if the swap is internal and should skip hook operations
|
|
505
|
+
function _isInternalSwap(address sender) internal view returns (bool) {
|
|
506
|
+
return sender == address(this) || sender == address(zoraLimitOrderBook) || zoraHookRegistry.isRegisteredHook(sender);
|
|
507
|
+
}
|
|
508
|
+
|
|
398
509
|
/// @notice Receives ETH from the pool manager for ETH-backed coins during fee collection.
|
|
399
510
|
/// @dev Only required for coins using ETH as backing currency (currency = address(0)).
|
|
400
511
|
/// Restricted to onlyPoolManager to prevent ETH from getting stuck in the contract.
|
|
@@ -17,6 +17,7 @@ import {ICoinV3} from "../../interfaces/ICoinV3.sol";
|
|
|
17
17
|
import {CoinConfigurationVersions} from "../../libs/CoinConfigurationVersions.sol";
|
|
18
18
|
import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
|
|
19
19
|
import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
|
|
20
|
+
import {V3ToV4SwapLib} from "../../libs/V3ToV4SwapLib.sol";
|
|
20
21
|
|
|
21
22
|
/// @title BuySupplyWithV4SwapHook
|
|
22
23
|
/// @notice Hook for purchasing initial coin supply with flexible swap routing
|
|
@@ -67,9 +68,6 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
|
67
68
|
// ============ ERRORS ============
|
|
68
69
|
|
|
69
70
|
error OnlyPoolManager();
|
|
70
|
-
error InsufficientInputCurrency(uint256 inputAmount, uint256 availableAmount);
|
|
71
|
-
error V3RouteCannotStartWithInputCurrency();
|
|
72
|
-
error V3RouteDoesNotConnectToV4RouteStart();
|
|
73
71
|
error InsufficientOutputAmount();
|
|
74
72
|
|
|
75
73
|
// ============ CONSTRUCTOR ============
|
|
@@ -90,12 +88,20 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
|
90
88
|
PoolKey[] memory v4Route = _buildV4RouteToCoin(coin, params.v4Route);
|
|
91
89
|
|
|
92
90
|
// STEP 2: Validate routes
|
|
93
|
-
|
|
91
|
+
V3ToV4SwapLib.validateRoutes(params.v3Route, params.inputCurrency, v4Route);
|
|
94
92
|
|
|
95
|
-
|
|
93
|
+
V3ToV4SwapLib.validateAndTransferInputCurrency(params.inputCurrency, params.inputAmount, params.buyRecipient, msg.value);
|
|
96
94
|
|
|
97
95
|
// STEP 3: Execute V3 swap (inputCurrency -> backing currency)
|
|
98
|
-
(uint256 currencyAmount, address currencyReceived) =
|
|
96
|
+
(uint256 currencyAmount, address currencyReceived) = V3ToV4SwapLib.executeV3Swap(
|
|
97
|
+
swapRouter,
|
|
98
|
+
V3ToV4SwapLib.V3SwapParams({
|
|
99
|
+
v3Route: params.v3Route,
|
|
100
|
+
inputCurrency: params.inputCurrency,
|
|
101
|
+
inputAmount: params.inputAmount,
|
|
102
|
+
recipient: address(this)
|
|
103
|
+
})
|
|
104
|
+
);
|
|
99
105
|
|
|
100
106
|
// STEP 4: Execute V4 swaps if needed, then buy coin
|
|
101
107
|
uint256 coinAmount = _executeV4Swap(v4Route, currencyAmount, currencyReceived, params.buyRecipient);
|
|
@@ -120,41 +126,6 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
|
120
126
|
|
|
121
127
|
// ============ VALIDATION ============
|
|
122
128
|
|
|
123
|
-
function _validateRoutes(InitialSupplyParams memory params, PoolKey[] memory v4Route) internal pure {
|
|
124
|
-
// Determine what currency should be the input to the V4 route
|
|
125
|
-
address v4InputCurrency;
|
|
126
|
-
if (params.v3Route.length == 0) {
|
|
127
|
-
// No V3 swap - input currency should directly match V4 route start
|
|
128
|
-
v4InputCurrency = params.inputCurrency;
|
|
129
|
-
} else {
|
|
130
|
-
// V3 swap exists - V3 output should match V4 route start
|
|
131
|
-
v4InputCurrency = _getV3RouteOutputCurrency(params.v3Route);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
PoolKey memory firstPool = v4Route[0];
|
|
135
|
-
|
|
136
|
-
require(
|
|
137
|
-
v4InputCurrency == Currency.unwrap(firstPool.currency0) || v4InputCurrency == Currency.unwrap(firstPool.currency1),
|
|
138
|
-
V3RouteDoesNotConnectToV4RouteStart()
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function _validateAndTransferInputCurrency(InitialSupplyParams memory params) internal {
|
|
143
|
-
if (params.inputCurrency == address(0)) {
|
|
144
|
-
uint256 providedAmount = msg.value;
|
|
145
|
-
|
|
146
|
-
require(providedAmount == params.inputAmount, InsufficientInputCurrency(params.inputAmount, providedAmount));
|
|
147
|
-
} else {
|
|
148
|
-
uint256 providedAmount = IERC20(params.inputCurrency).allowance(params.buyRecipient, address(this));
|
|
149
|
-
|
|
150
|
-
// must be enough allowance to transfer
|
|
151
|
-
require(providedAmount >= params.inputAmount, InsufficientInputCurrency(params.inputAmount, providedAmount));
|
|
152
|
-
|
|
153
|
-
// transfer from the buy recipient to this contract
|
|
154
|
-
IERC20(params.inputCurrency).safeTransferFrom(params.buyRecipient, address(this), params.inputAmount);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
129
|
function _buildV4RouteToCoin(ICoin coin, PoolKey[] memory v4Route) internal view returns (PoolKey[] memory fullRoute) {
|
|
159
130
|
fullRoute = new PoolKey[](v4Route.length + 1);
|
|
160
131
|
|
|
@@ -165,31 +136,7 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
|
165
136
|
fullRoute[v4Route.length] = coin.getPoolKey();
|
|
166
137
|
}
|
|
167
138
|
|
|
168
|
-
// ============
|
|
169
|
-
|
|
170
|
-
function _executeV3Swap(InitialSupplyParams memory params) internal returns (uint256 amountCurrency, address currencyReceived) {
|
|
171
|
-
if (params.v3Route.length == 0) {
|
|
172
|
-
// No V3 swap needed - return inputAmount directly
|
|
173
|
-
return (params.inputAmount, params.inputCurrency);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// for v3 swap section, we dont support currently having an input currency other than eth
|
|
177
|
-
if (params.inputCurrency != address(0)) {
|
|
178
|
-
revert V3RouteCannotStartWithInputCurrency();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Build swap router call for exactInput
|
|
182
|
-
ISwapRouter.ExactInputParams memory swapParams = ISwapRouter.ExactInputParams({
|
|
183
|
-
path: params.v3Route,
|
|
184
|
-
recipient: address(this),
|
|
185
|
-
amountIn: params.inputAmount,
|
|
186
|
-
amountOutMinimum: 0 // For testing - in production should have slippage protection
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
amountCurrency = swapRouter.exactInput{value: params.inputAmount}(swapParams);
|
|
190
|
-
|
|
191
|
-
currencyReceived = _getV3RouteOutputCurrency(params.v3Route);
|
|
192
|
-
}
|
|
139
|
+
// ============ V4 SWAP LOGIC ============
|
|
193
140
|
|
|
194
141
|
function _executeV4Swap(PoolKey[] memory v4Route, uint256 amountIn, address currencyIn, address buyRecipient) internal returns (uint256 amountCoin) {
|
|
195
142
|
Currency startingCurrency = Currency.wrap(currencyIn);
|
|
@@ -207,37 +154,16 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
|
207
154
|
(PoolKey[], uint256, Currency, address)
|
|
208
155
|
);
|
|
209
156
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
for (uint256 i = 0; i < v4Route.length; i++) {
|
|
216
|
-
PoolKey memory poolKey = v4Route[i];
|
|
217
|
-
|
|
218
|
-
// Determine swap direction based on current currency
|
|
219
|
-
bool zeroForOne = lastReceivedCurrency == poolKey.currency0;
|
|
220
|
-
|
|
221
|
-
BalanceDelta delta = poolManager.swap(
|
|
222
|
-
poolKey,
|
|
223
|
-
SwapParams(zeroForOne, -(int128(lastReceivedAmount)), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
|
|
224
|
-
""
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
// Extract output amount from delta
|
|
228
|
-
outputAmount = zeroForOne ? uint128(delta.amount1()) : uint128(delta.amount0());
|
|
229
|
-
|
|
230
|
-
// Update currentAmount for next iteration
|
|
231
|
-
lastReceivedAmount = uint128(outputAmount);
|
|
232
|
-
|
|
233
|
-
// Update current currency for next swap
|
|
234
|
-
lastReceivedCurrency = zeroForOne ? poolKey.currency1 : poolKey.currency0;
|
|
235
|
-
}
|
|
157
|
+
// Execute V4 multi-hop swap
|
|
158
|
+
V3ToV4SwapLib.V4SwapResult memory result = V3ToV4SwapLib.executeV4MultiHopSwap(
|
|
159
|
+
poolManager,
|
|
160
|
+
V3ToV4SwapLib.V4SwapParams({v4Route: v4Route, amountIn: amountIn, startingCurrency: startingCurrency})
|
|
161
|
+
);
|
|
236
162
|
|
|
237
163
|
// Settle all currency deltas and get final amount
|
|
238
|
-
|
|
164
|
+
V3ToV4SwapLib.settleDeltas(poolManager, startingCurrency, result.outputCurrency, buyRecipient, amountIn, result.outputAmount);
|
|
239
165
|
|
|
240
|
-
return abi.encode(
|
|
166
|
+
return abi.encode(result.outputAmount);
|
|
241
167
|
}
|
|
242
168
|
|
|
243
169
|
/// @notice Helper to decode V4 route data (external for try/catch)
|
|
@@ -249,22 +175,6 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
|
249
175
|
return abi.encode(params);
|
|
250
176
|
}
|
|
251
177
|
|
|
252
|
-
function _settleDeltas(Currency inputCurrency, Currency outputCurrency, address to, uint256 inputAmount, uint128 outputAmount) private {
|
|
253
|
-
// pay the input amount
|
|
254
|
-
if (inputCurrency.isAddressZero()) {
|
|
255
|
-
// For ETH, settle with msg.value
|
|
256
|
-
poolManager.settle{value: inputAmount}();
|
|
257
|
-
} else {
|
|
258
|
-
// For ERC20, sync and transfer
|
|
259
|
-
poolManager.sync(inputCurrency);
|
|
260
|
-
inputCurrency.transfer(address(poolManager), inputAmount);
|
|
261
|
-
poolManager.settle();
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// transfer the output amount to the recipient
|
|
265
|
-
poolManager.take(outputCurrency, to, outputAmount);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
178
|
// ============ UTILITIES ============
|
|
269
179
|
|
|
270
180
|
function _getCoinBackingCurrency(ICoin coin) internal view returns (Currency) {
|
|
@@ -275,36 +185,4 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
|
275
185
|
}
|
|
276
186
|
return poolKey.currency0;
|
|
277
187
|
}
|
|
278
|
-
|
|
279
|
-
function _getV3RouteOutputCurrency(bytes memory path) internal pure returns (address tokenOut) {
|
|
280
|
-
if (path.length == 0) {
|
|
281
|
-
// if no path, then output currency is eth
|
|
282
|
-
return address(0);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// For a path with multiple pools, we need to traverse to the end
|
|
286
|
-
// Path format: tokenA + fee + tokenB + fee + tokenC...
|
|
287
|
-
// We want the final token (tokenC in this example)
|
|
288
|
-
|
|
289
|
-
// Follow Uniswap's pattern: traverse the path to find the final token
|
|
290
|
-
bytes memory currentPath = path;
|
|
291
|
-
|
|
292
|
-
// Keep skipping tokens until we reach the final pool
|
|
293
|
-
while (currentPath.hasMultiplePools()) {
|
|
294
|
-
currentPath = currentPath.skipToken();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// The final segment contains the last pool, decode to get the output token
|
|
298
|
-
(, tokenOut, ) = currentPath.decodeFirstPool();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function _getV3RouteInputCurrency(bytes memory path) internal pure returns (address tokenIn) {
|
|
302
|
-
if (path.length == 0) {
|
|
303
|
-
// if no path, then input currency is eth
|
|
304
|
-
return address(0);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Use Path library to get the input token (first token in the path)
|
|
308
|
-
(tokenIn, , ) = path.decodeFirstPool();
|
|
309
|
-
}
|
|
310
188
|
}
|