@zoralabs/coins 1.0.0 → 1.0.1
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/.turbo/turbo-build.log +123 -75
- package/CHANGELOG.md +6 -0
- package/abis/BuySupplyWithSwapRouterHook.json +40 -0
- package/abis/CoinUniV4Test.json +42 -4
- package/abis/FeeEstimatorHook.json +0 -13
- package/abis/HooksTest.json +13 -0
- package/abis/IZoraFactory.json +19 -0
- package/abis/UpgradesTest.json +14 -0
- package/abis/ZoraV4CoinHook.json +0 -13
- package/addresses/8453.json +7 -8
- package/addresses/84532.json +8 -9
- package/addresses/dev/8453.json +10 -0
- package/dist/index.cjs +15 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +30 -0
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/foundry.toml +1 -0
- package/package/wagmiGenerated.ts +14 -0
- package/package.json +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/DeployDevFactory.s.sol +2 -2
- package/script/DeployHooks.s.sol +1 -1
- package/script/PrintUpgradeCommand.s.sol +1 -1
- package/script/TestBackingCoinSwap.s.sol +25 -24
- package/script/TestV4Swap.s.sol +6 -6
- package/script/UpgradeCoinImpl.sol +2 -2
- package/script/UpgradeFactoryImpl.s.sol +1 -1
- package/src/BaseCoin.sol +14 -0
- package/{script → src/deployment}/CoinsDeployerBase.sol +59 -28
- package/src/hooks/ZoraV4CoinHook.sol +32 -15
- package/src/hooks/deployment/BuySupplyWithSwapRouterHook.sol +64 -4
- package/src/interfaces/IZoraFactory.sol +2 -1
- package/src/libs/CoinRewardsV4.sol +0 -1
- package/src/libs/HooksDeployment.sol +51 -7
- package/src/libs/UniV4SwapToCurrency.sol +3 -3
- package/src/libs/V4Liquidity.sol +2 -9
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/CoinUniV4.t.sol +85 -60
- package/test/DeploymentHooks.t.sol +51 -10
- package/test/Upgrades.t.sol +92 -1
- package/test/utils/BaseTest.sol +56 -1
|
@@ -9,20 +9,31 @@ import {IWETH} from "../../interfaces/IWETH.sol";
|
|
|
9
9
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
10
10
|
import {Coin} from "../../Coin.sol";
|
|
11
11
|
import {ICoinV3} from "../../interfaces/ICoinV3.sol";
|
|
12
|
+
import {ICoinV4} from "../../interfaces/ICoinV4.sol";
|
|
13
|
+
import {CoinConfigurationVersions} from "../../libs/CoinConfigurationVersions.sol";
|
|
14
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
15
|
+
import {IPoolManager, SwapParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
16
|
+
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
|
17
|
+
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
12
18
|
|
|
13
19
|
/// @title BuySupplyWithSwapRouter
|
|
14
|
-
/// @notice A hook that buys supply for a coin that is priced in an erc20 token
|
|
15
|
-
/// Supports both single-hop and multi-hop swaps using uniswap v3
|
|
20
|
+
/// @notice A hook that buys supply for a coin that is priced in an erc20 token a backing currency, using a Uniswap V3 SwapRouter.
|
|
21
|
+
/// Supports both single-hop and multi-hop swaps using uniswap v3. Supports buying the coin supply whether the coin is a v3 or v4 coin.
|
|
22
|
+
/// @author @oveddan
|
|
16
23
|
contract BuySupplyWithSwapRouterHook is BaseCoinDeployHook {
|
|
17
24
|
ISwapRouter immutable swapRouter;
|
|
25
|
+
IPoolManager immutable poolManager;
|
|
26
|
+
using BalanceDeltaLibrary for BalanceDelta;
|
|
18
27
|
|
|
19
28
|
error Erc20NotReceived();
|
|
20
29
|
error InvalidSwapRouterCall();
|
|
21
30
|
error SwapReverted(bytes error);
|
|
22
31
|
error CoinBalanceNot0(uint256 balance);
|
|
32
|
+
error CurrencyBalanceNot0(uint256 balance);
|
|
23
33
|
|
|
24
|
-
constructor(IZoraFactory _factory, address _swapRouter) BaseCoinDeployHook(_factory) {
|
|
34
|
+
constructor(IZoraFactory _factory, address _swapRouter, address _poolManager) BaseCoinDeployHook(_factory) {
|
|
25
35
|
swapRouter = ISwapRouter(_swapRouter);
|
|
36
|
+
poolManager = IPoolManager(_poolManager);
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
/// @notice Hook that buys supply for a coin that is priced in an erc20 token with ETH, using a Uniswap SwapRouter.
|
|
@@ -71,10 +82,59 @@ contract BuySupplyWithSwapRouterHook is BaseCoinDeployHook {
|
|
|
71
82
|
function _handleBuy(address buyRecipient, ICoin coin, uint256 amountCurrency) internal returns (uint256 coinsPurchased) {
|
|
72
83
|
IERC20(coin.currency()).approve(address(coin), amountCurrency);
|
|
73
84
|
|
|
74
|
-
|
|
85
|
+
if (CoinConfigurationVersions.isV4(factory.getVersionForDeployedCoin(address(coin)))) {
|
|
86
|
+
coinsPurchased = _executeV4Buy(buyRecipient, ICoinV4(payable(address(coin))), amountCurrency);
|
|
87
|
+
} else {
|
|
88
|
+
coinsPurchased = _executeV3Buy(buyRecipient, ICoinV3(payable(address(coin))), amountCurrency);
|
|
89
|
+
}
|
|
75
90
|
|
|
76
91
|
// make sure that this contract has no balance of the coin remaining
|
|
77
92
|
uint256 coinBalance = IERC20(address(coin)).balanceOf(address(this));
|
|
78
93
|
require(coinBalance == 0, CoinBalanceNot0(coinBalance));
|
|
94
|
+
// make sure that this contract has no balance of the currency remaining
|
|
95
|
+
uint256 currencyBalance = IERC20(coin.currency()).balanceOf(address(this));
|
|
96
|
+
require(currencyBalance == 0, CurrencyBalanceNot0(currencyBalance));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _executeV3Buy(address buyRecipient, ICoinV3 coin, uint256 amountCurrency) internal returns (uint256 coinsPurchased) {
|
|
100
|
+
(, coinsPurchased) = ICoinV3(payable(address(coin))).buy(buyRecipient, amountCurrency, 0, 0, address(0));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _executeV4Buy(address buyRecipient, ICoinV4 coin, uint256 amountCurrency) internal returns (uint256 coinsPurchased) {
|
|
104
|
+
bytes memory data = abi.encode(buyRecipient, coin, amountCurrency);
|
|
105
|
+
|
|
106
|
+
bytes memory result = poolManager.unlock(data);
|
|
107
|
+
|
|
108
|
+
coinsPurchased = abi.decode(result, (uint256));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
error OnlyPoolManager();
|
|
112
|
+
|
|
113
|
+
/// @notice Internal fn called when the PoolManager is unlocked. Used to swap the backing currency for the coin.
|
|
114
|
+
function unlockCallback(bytes calldata data) external returns (bytes memory) {
|
|
115
|
+
require(msg.sender == address(poolManager), OnlyPoolManager());
|
|
116
|
+
|
|
117
|
+
(address buyRecipient, ICoinV4 coin, uint256 amountCurrency) = abi.decode(data, (address, ICoinV4, uint256));
|
|
118
|
+
|
|
119
|
+
bool zeroForOne = coin.currency() == Currency.unwrap(coin.getPoolKey().currency0);
|
|
120
|
+
|
|
121
|
+
BalanceDelta delta = poolManager.swap(
|
|
122
|
+
coin.getPoolKey(),
|
|
123
|
+
SwapParams(zeroForOne, -(int128(uint128(amountCurrency))), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
|
|
124
|
+
""
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
int128 amountCoin = zeroForOne ? delta.amount1() : delta.amount0();
|
|
128
|
+
|
|
129
|
+
// sync the currency balance before transferring to the pool manager
|
|
130
|
+
poolManager.sync(Currency.wrap(coin.currency()));
|
|
131
|
+
// transfer the currency to the pool manager for the swap
|
|
132
|
+
IERC20(coin.currency()).transfer(address(poolManager), uint256(uint128(amountCurrency)));
|
|
133
|
+
// collect the coin from the pool manager
|
|
134
|
+
poolManager.take(Currency.wrap(address(coin)), buyRecipient, uint256(uint128(amountCoin)));
|
|
135
|
+
|
|
136
|
+
poolManager.settle();
|
|
137
|
+
|
|
138
|
+
return abi.encode(uint256(uint128(amountCoin)));
|
|
79
139
|
}
|
|
80
140
|
}
|
|
@@ -3,8 +3,9 @@ pragma solidity ^0.8.23;
|
|
|
3
3
|
|
|
4
4
|
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
5
5
|
import {PoolKeyStruct} from "./ICoin.sol";
|
|
6
|
+
import {IDeployedCoinVersionLookup} from "./IDeployedCoinVersionLookup.sol";
|
|
6
7
|
|
|
7
|
-
interface IZoraFactory {
|
|
8
|
+
interface IZoraFactory is IDeployedCoinVersionLookup {
|
|
8
9
|
/// @notice Emitted when a coin is created
|
|
9
10
|
/// @param caller The msg.sender address
|
|
10
11
|
/// @param payoutRecipient The address of the creator payout recipient
|
|
@@ -52,7 +52,6 @@ library CoinRewardsV4 {
|
|
|
52
52
|
// Step 1: Collect accrued fees from all LP positions in both token0 and token1
|
|
53
53
|
(fees0, fees1) = V4Liquidity.collectFees(poolManager, key, positions);
|
|
54
54
|
|
|
55
|
-
// Step 2: Swap the collected fees through the specified path to convert them to the target payout currency
|
|
56
55
|
// This handles multi-hop swaps if needed (e.g. coin -> backingCoin -> backingCoin's currency)
|
|
57
56
|
(receivedCurrency, receivedAmount) = UniV4SwapToCurrency.swapToPath(
|
|
58
57
|
poolManager,
|
|
@@ -17,6 +17,10 @@ library HookMinerWithCreationCodeArgs {
|
|
|
17
17
|
// (arbitrarily set)
|
|
18
18
|
uint256 constant MAX_LOOP = 160_444;
|
|
19
19
|
|
|
20
|
+
function deterministicHookAddress(address deployer, bytes32 salt, bytes memory creationCode) internal view returns (address) {
|
|
21
|
+
return Create2.computeAddress(salt, keccak256(creationCode), deployer);
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
/// @notice Find a salt that produces a hook address with the desired `flags`
|
|
21
25
|
/// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address
|
|
22
26
|
/// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy)
|
|
@@ -30,7 +34,7 @@ library HookMinerWithCreationCodeArgs {
|
|
|
30
34
|
|
|
31
35
|
bytes32 creationCodeHash = keccak256(creationCodeWithArgs);
|
|
32
36
|
for (uint256 salt; salt < MAX_LOOP; salt++) {
|
|
33
|
-
hookAddress =
|
|
37
|
+
hookAddress = deterministicHookAddress(deployer, bytes32(salt), creationCodeWithArgs);
|
|
34
38
|
|
|
35
39
|
// if the hook's bottom 14 bits match the desired flags AND the address does not have bytecode, we found a match
|
|
36
40
|
if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) {
|
|
@@ -45,10 +49,17 @@ library HooksDeployment {
|
|
|
45
49
|
error HookNotDeployed();
|
|
46
50
|
error InvalidHookAddress(address expected, address actual);
|
|
47
51
|
|
|
48
|
-
function
|
|
52
|
+
function mineForSaltAndDeployHook(address deployer, bytes memory hookCreationCode) internal returns (IHooks hook, bytes32 salt) {
|
|
49
53
|
uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG) ^ (0x4444 << 144);
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
address hookAddress;
|
|
56
|
+
(hookAddress, salt) = HookMinerWithCreationCodeArgs.find(deployer, flags, hookCreationCode);
|
|
57
|
+
|
|
58
|
+
// check if the hook is already deployed
|
|
59
|
+
(bool isDeployed, address existingHookAddress) = hooksIsDeployed(deployer, hookCreationCode, salt);
|
|
60
|
+
if (isDeployed) {
|
|
61
|
+
return (IHooks(existingHookAddress), salt);
|
|
62
|
+
}
|
|
52
63
|
|
|
53
64
|
hook = IHooks(Create2.deploy(0, salt, hookCreationCode));
|
|
54
65
|
|
|
@@ -57,6 +68,32 @@ library HooksDeployment {
|
|
|
57
68
|
require(hookAddress == address(hook), InvalidHookAddress(hookAddress, address(hook)));
|
|
58
69
|
}
|
|
59
70
|
|
|
71
|
+
/// @notice Checks if ZoraV4CoinHook is already deployed for given parameters
|
|
72
|
+
/// @param deployer The address that will deploy the hook
|
|
73
|
+
/// @param hookCreationCode The creation code of the hook
|
|
74
|
+
/// @param existingSalt The salt of the existing hook
|
|
75
|
+
/// @return isDeployed True if hook is already deployed
|
|
76
|
+
/// @return hookAddress The address where the hook would be/is deployed
|
|
77
|
+
function hooksIsDeployed(
|
|
78
|
+
address deployer,
|
|
79
|
+
bytes memory hookCreationCode,
|
|
80
|
+
bytes32 existingSalt
|
|
81
|
+
) internal view returns (bool isDeployed, address hookAddress) {
|
|
82
|
+
hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, existingSalt, hookCreationCode);
|
|
83
|
+
|
|
84
|
+
// Check if code exists at the predicted address
|
|
85
|
+
isDeployed = hookAddress.code.length > 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deployOrGetHook(address deployer, bytes memory hookCreationCode, bytes32 existingSalt) internal returns (IHooks hook, bytes32 salt) {
|
|
89
|
+
(bool isDeployed, address existingHookAddress) = hooksIsDeployed(deployer, hookCreationCode, existingSalt);
|
|
90
|
+
if (isDeployed) {
|
|
91
|
+
return (IHooks(existingHookAddress), existingSalt);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
(hook, salt) = mineForSaltAndDeployHook(deployer, hookCreationCode);
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
function zoraV4CoinHookCreationCode(
|
|
61
98
|
address poolManager,
|
|
62
99
|
address coinVersionLookup,
|
|
@@ -70,15 +107,22 @@ library HooksDeployment {
|
|
|
70
107
|
address coinVersionLookup,
|
|
71
108
|
address[] memory trustedMessageSenders
|
|
72
109
|
) internal returns (IHooks hook) {
|
|
73
|
-
|
|
110
|
+
bytes memory hookCreationCode = zoraV4CoinHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders);
|
|
111
|
+
(hook, ) = deployOrGetHook(address(this), hookCreationCode, bytes32(0));
|
|
74
112
|
}
|
|
75
113
|
|
|
114
|
+
/// @notice Deploys or returns existing ZoraV4CoinHook using deterministic deployment. Ensures that if a hooks is already
|
|
115
|
+
/// deployed with an existing salt, it will be returned.
|
|
76
116
|
function deployZoraV4CoinHookFromScript(
|
|
77
117
|
address poolManager,
|
|
78
118
|
address coinVersionLookup,
|
|
79
|
-
address[] memory trustedMessageSenders
|
|
80
|
-
|
|
119
|
+
address[] memory trustedMessageSenders,
|
|
120
|
+
bytes32 existingSalt
|
|
121
|
+
) internal returns (IHooks hook, bytes32 salt) {
|
|
81
122
|
address deployer = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
|
|
82
|
-
|
|
123
|
+
|
|
124
|
+
bytes memory hookCreationCode = zoraV4CoinHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders);
|
|
125
|
+
|
|
126
|
+
(hook, salt) = deployOrGetHook(deployer, hookCreationCode, existingSalt);
|
|
83
127
|
}
|
|
84
128
|
}
|
|
@@ -47,17 +47,17 @@ library UniV4SwapToCurrency {
|
|
|
47
47
|
) private returns (Currency outputCurrency, uint128 outputAmount) {
|
|
48
48
|
(PoolKey memory poolKey, bool zeroForOne) = _getPoolAndSwapDirection(pathKey, coin);
|
|
49
49
|
|
|
50
|
-
uint128
|
|
50
|
+
uint128 inputAmount = zeroForOne ? amount0 : amount1;
|
|
51
51
|
|
|
52
52
|
outputCurrency = zeroForOne ? poolKey.currency1 : poolKey.currency0;
|
|
53
53
|
|
|
54
54
|
uint128 initialAmountCurrency = zeroForOne ? amount1 : amount0;
|
|
55
55
|
|
|
56
56
|
// if not swapping any coin for currency, output amount is amount of currency
|
|
57
|
-
if (
|
|
57
|
+
if (inputAmount == 0) {
|
|
58
58
|
outputAmount = initialAmountCurrency;
|
|
59
59
|
} else {
|
|
60
|
-
outputAmount = uint128(_swap(poolManager, poolKey, zeroForOne, -int128(
|
|
60
|
+
outputAmount = initialAmountCurrency + uint128(_swap(poolManager, poolKey, zeroForOne, -int128(inputAmount), bytes("")));
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
package/src/libs/V4Liquidity.sol
CHANGED
|
@@ -43,19 +43,12 @@ library V4Liquidity {
|
|
|
43
43
|
IPoolManager(poolManager).unlock(data);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function handleMintPositionsCallback(IPoolManager poolManager, bytes memory data) internal
|
|
46
|
+
function handleMintPositionsCallback(IPoolManager poolManager, bytes memory data) internal {
|
|
47
47
|
CallbackData memory callbackData = abi.decode(data, (CallbackData));
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
uint256 amount1;
|
|
51
|
-
int128 fees0;
|
|
52
|
-
int128 fees1;
|
|
53
|
-
|
|
54
|
-
(fees0, fees1) = _mintPositions(poolManager, callbackData.poolKey, callbackData.positions);
|
|
49
|
+
_mintPositions(poolManager, callbackData.poolKey, callbackData.positions);
|
|
55
50
|
|
|
56
51
|
_settleUp(poolManager, callbackData.poolKey);
|
|
57
|
-
|
|
58
|
-
return UnlockData({amount0: amount0, amount1: amount1, fees0: fees0, fees1: fees1});
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
function collectFees(IPoolManager poolManager, PoolKey memory poolKey, LpPosition[] storage positions) internal returns (int128 balance0, int128 balance1) {
|
|
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
|
|
|
9
9
|
contract ContractVersionBase is IVersionedContract {
|
|
10
10
|
/// @notice The version of the contract
|
|
11
11
|
function contractVersion() external pure override returns (string memory) {
|
|
12
|
-
return "1.0.
|
|
12
|
+
return "1.0.1";
|
|
13
13
|
}
|
|
14
14
|
}
|
package/test/CoinUniV4.t.sol
CHANGED
|
@@ -36,8 +36,7 @@ contract CoinUniV4Test is BaseTest {
|
|
|
36
36
|
CoinV4 internal coinV4;
|
|
37
37
|
|
|
38
38
|
IPoolManager internal poolManager;
|
|
39
|
-
|
|
40
|
-
IUniversalRouter internal router;
|
|
39
|
+
|
|
41
40
|
IV4Quoter internal quoter;
|
|
42
41
|
MockERC20 internal mockERC20A;
|
|
43
42
|
MockERC20 internal mockERC20B;
|
|
@@ -46,8 +45,7 @@ contract CoinUniV4Test is BaseTest {
|
|
|
46
45
|
super.setUpWithBlockNumber(30267794);
|
|
47
46
|
|
|
48
47
|
poolManager = IPoolManager(V4_POOL_MANAGER);
|
|
49
|
-
|
|
50
|
-
router = IUniversalRouter(UNIVERSAL_ROUTER);
|
|
48
|
+
|
|
51
49
|
quoter = IV4Quoter(V4_QUOTER);
|
|
52
50
|
mockERC20A = new MockERC20("MockERC20A", "MCKA");
|
|
53
51
|
mockERC20B = new MockERC20("MockERC20B", "MCKB");
|
|
@@ -67,6 +65,9 @@ contract CoinUniV4Test is BaseTest {
|
|
|
67
65
|
return _deployV4Coin(currency, address(0), salt);
|
|
68
66
|
}
|
|
69
67
|
|
|
68
|
+
string constant DEFAULT_NAME = "Testcoin";
|
|
69
|
+
string constant DEFAULT_SYMBOL = "TEST";
|
|
70
|
+
|
|
70
71
|
function _deployV4Coin(address currency, address createReferral, bytes32 salt) internal returns (ICoinV4) {
|
|
71
72
|
address[] memory owners = new address[](1);
|
|
72
73
|
owners[0] = users.creator;
|
|
@@ -78,8 +79,8 @@ contract CoinUniV4Test is BaseTest {
|
|
|
78
79
|
users.creator,
|
|
79
80
|
owners,
|
|
80
81
|
"https://test.com",
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
DEFAULT_NAME,
|
|
83
|
+
DEFAULT_SYMBOL,
|
|
83
84
|
poolConfig,
|
|
84
85
|
createReferral,
|
|
85
86
|
address(0),
|
|
@@ -91,6 +92,10 @@ contract CoinUniV4Test is BaseTest {
|
|
|
91
92
|
return coinV4;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
function _getCoinAddress(address currency, address createReferral, bytes32 salt) internal view returns (address) {
|
|
96
|
+
return factory.coinAddress(users.creator, DEFAULT_NAME, DEFAULT_SYMBOL, _defaultPoolConfig(currency), createReferral, salt);
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
/// @dev Estimates the fees from a swap, by deploying a test hook that doesn't distribute the fees
|
|
95
100
|
/// and then reverting the state after the swap
|
|
96
101
|
function _estimateLpFees(bytes memory commands, bytes[] memory inputs) internal returns (FeeEstimatorHook.FeeEstimatorState memory feeState) {
|
|
@@ -106,50 +111,6 @@ contract CoinUniV4Test is BaseTest {
|
|
|
106
111
|
vm.revertToState(snapshot);
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
function _swapSomeCurrencyForCoin(ICoinV4 _coin, address currency, uint128 amountIn, address trader) internal {
|
|
110
|
-
uint128 minAmountOut = uint128(0);
|
|
111
|
-
|
|
112
|
-
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
113
|
-
currency,
|
|
114
|
-
amountIn,
|
|
115
|
-
address(_coin),
|
|
116
|
-
minAmountOut,
|
|
117
|
-
_coin.getPoolKey(),
|
|
118
|
-
bytes("")
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
vm.startPrank(trader);
|
|
122
|
-
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), currency, amountIn, uint48(block.timestamp + 1 days));
|
|
123
|
-
|
|
124
|
-
// Execute the swap
|
|
125
|
-
uint256 deadline = block.timestamp + 20;
|
|
126
|
-
router.execute(commands, inputs, deadline);
|
|
127
|
-
|
|
128
|
-
vm.stopPrank();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function _swapSomeCoinForCurrency(ICoinV4 _coin, address currency, uint128 amountIn, address trader) internal {
|
|
132
|
-
uint128 minAmountOut = uint128(0);
|
|
133
|
-
|
|
134
|
-
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
135
|
-
address(_coin),
|
|
136
|
-
amountIn,
|
|
137
|
-
currency,
|
|
138
|
-
minAmountOut,
|
|
139
|
-
_coin.getPoolKey(),
|
|
140
|
-
bytes("")
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
vm.startPrank(trader);
|
|
144
|
-
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(_coin), amountIn, uint48(block.timestamp + 1 days));
|
|
145
|
-
|
|
146
|
-
// Execute the swap
|
|
147
|
-
uint256 deadline = block.timestamp + 20;
|
|
148
|
-
router.execute(commands, inputs, deadline);
|
|
149
|
-
|
|
150
|
-
vm.stopPrank();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
114
|
/// and then reverting the state after the swap
|
|
154
115
|
function _estimateSwap(
|
|
155
116
|
bytes memory commands,
|
|
@@ -455,14 +416,57 @@ contract CoinUniV4Test is BaseTest {
|
|
|
455
416
|
assertGt(amountOut, 0);
|
|
456
417
|
}
|
|
457
418
|
|
|
458
|
-
function
|
|
419
|
+
function _findCoinAddress(address currency, bool isCoinToken0) internal view returns (bytes32 salt, address coinAddress) {
|
|
420
|
+
uint256 i = 0;
|
|
421
|
+
|
|
422
|
+
while (true) {
|
|
423
|
+
salt = bytes32(keccak256(abi.encode(i)));
|
|
424
|
+
coinAddress = _getCoinAddress(currency, address(0), salt);
|
|
425
|
+
bool coinIsToken0 = coinAddress < currency;
|
|
426
|
+
if (coinIsToken0 == isCoinToken0) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
i++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function test_canSwapCurrencyForCoinCoinIsFirst(uint128 amountIn) public {
|
|
434
|
+
vm.assume(amountIn > 0.00001 ether);
|
|
435
|
+
vm.assume(amountIn < 10000000000000 ether);
|
|
459
436
|
address currency = address(mockERC20A);
|
|
460
|
-
_deployV4Coin(currency);
|
|
461
437
|
|
|
462
|
-
|
|
438
|
+
(bytes32 salt, ) = _findCoinAddress(currency, true);
|
|
439
|
+
|
|
440
|
+
_deployV4Coin(currency, address(0), salt);
|
|
441
|
+
|
|
442
|
+
bool isCoinToken0 = CoinCommon.sortTokens(address(coinV4), currency);
|
|
443
|
+
|
|
444
|
+
assertTrue(isCoinToken0);
|
|
445
|
+
|
|
446
|
+
_testSwapCurrencyForCoin(currency, amountIn);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function test_canSwapCurrencyForCoinCoinIsSecond(uint128 amountIn) public {
|
|
450
|
+
vm.assume(amountIn > 0.00001 ether);
|
|
451
|
+
vm.assume(amountIn < 10000000000000 ether);
|
|
452
|
+
|
|
453
|
+
// make a currency with a small number, that will always be less than the coin
|
|
454
|
+
address currency = address(mockERC20A);
|
|
455
|
+
|
|
456
|
+
(bytes32 salt, ) = _findCoinAddress(currency, false);
|
|
457
|
+
|
|
458
|
+
_deployV4Coin(currency, address(0), salt);
|
|
463
459
|
|
|
460
|
+
assertTrue(coinV4.getPoolKey().currency0 == Currency.wrap(currency));
|
|
461
|
+
|
|
462
|
+
_testSwapCurrencyForCoin(currency, amountIn);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function _testSwapCurrencyForCoin(address currency, uint128 amountIn) private {
|
|
464
466
|
uint128 minAmountOut = 0;
|
|
465
467
|
|
|
468
|
+
MockERC20(currency).mint(address(poolManager), 100000000000000000000000000000 ether);
|
|
469
|
+
|
|
466
470
|
(bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
|
|
467
471
|
currency,
|
|
468
472
|
amountIn,
|
|
@@ -474,10 +478,8 @@ contract CoinUniV4Test is BaseTest {
|
|
|
474
478
|
|
|
475
479
|
address trader = makeAddr("trader");
|
|
476
480
|
|
|
477
|
-
uint128 initialTraderBalance = uint128(1 ether);
|
|
478
|
-
|
|
479
481
|
// mint some mockERC20 to the trader, so they can use it to buy the coin
|
|
480
|
-
mockERC20A.mint(trader,
|
|
482
|
+
mockERC20A.mint(trader, amountIn);
|
|
481
483
|
|
|
482
484
|
// have trader approve to permit2
|
|
483
485
|
vm.startPrank(trader);
|
|
@@ -487,16 +489,33 @@ contract CoinUniV4Test is BaseTest {
|
|
|
487
489
|
uint256 deadline = block.timestamp + 20;
|
|
488
490
|
router.execute(commands, inputs, deadline);
|
|
489
491
|
|
|
490
|
-
assertEq(mockERC20A.balanceOf(trader),
|
|
492
|
+
assertEq(mockERC20A.balanceOf(trader), 0);
|
|
491
493
|
assertGt(coinV4.balanceOf(trader), minAmountOut);
|
|
492
494
|
}
|
|
493
495
|
|
|
494
|
-
function
|
|
496
|
+
function test_canSwapCoinForCurrencyCoinIsFirst(uint128 amountIn) public {
|
|
497
|
+
vm.assume(amountIn > 0.00001 ether);
|
|
498
|
+
vm.assume(amountIn < 10000000000000 ether);
|
|
499
|
+
|
|
495
500
|
address currency = address(mockERC20A);
|
|
496
|
-
_deployV4Coin(currency);
|
|
497
501
|
|
|
498
|
-
|
|
502
|
+
(bytes32 salt, ) = _findCoinAddress(currency, true);
|
|
503
|
+
|
|
504
|
+
_deployV4Coin(currency, address(0), salt);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function test_canSwapCoinForCurrencyCoinIsSecond(uint128 amountIn) public {
|
|
508
|
+
vm.assume(amountIn > 0.00001 ether);
|
|
509
|
+
vm.assume(amountIn < 10000000000000 ether);
|
|
510
|
+
|
|
511
|
+
address currency = address(mockERC20A);
|
|
499
512
|
|
|
513
|
+
(bytes32 salt, ) = _findCoinAddress(currency, false);
|
|
514
|
+
|
|
515
|
+
_deployV4Coin(currency, address(0), salt);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function _testSwapCoinForCurrency(address currency, uint128 currencyIn) private {
|
|
500
519
|
address trader = makeAddr("trader");
|
|
501
520
|
|
|
502
521
|
mockERC20A.mint(trader, currencyIn);
|
|
@@ -519,6 +538,12 @@ contract CoinUniV4Test is BaseTest {
|
|
|
519
538
|
UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(coinV4), coinIn, uint48(block.timestamp + 1 days));
|
|
520
539
|
|
|
521
540
|
router.execute(commands, inputs, block.timestamp + 20);
|
|
541
|
+
|
|
542
|
+
// do some more swaps back and forth
|
|
543
|
+
_swapSomeCurrencyForCoin(coinV4, currency, uint128(IERC20(address(currency)).balanceOf(trader)), trader);
|
|
544
|
+
|
|
545
|
+
// swap back to coin
|
|
546
|
+
_swapSomeCoinForCurrency(coinV4, currency, uint128(IERC20(address(coinV4)).balanceOf(trader)), trader);
|
|
522
547
|
}
|
|
523
548
|
|
|
524
549
|
function testSwappingEmitsSwapEventFromSenderNoRevert() public {
|
|
@@ -23,6 +23,7 @@ contract FakeHookNoInterface {
|
|
|
23
23
|
|
|
24
24
|
contract HooksTest is BaseTest {
|
|
25
25
|
address constant zora = 0x1111111111166b7FE7bd91427724B487980aFc69;
|
|
26
|
+
BuySupplyWithSwapRouterHook hook;
|
|
26
27
|
|
|
27
28
|
function _generateDefaultPoolConfig(address currency) internal pure returns (bytes memory) {
|
|
28
29
|
return
|
|
@@ -38,9 +39,11 @@ contract HooksTest is BaseTest {
|
|
|
38
39
|
|
|
39
40
|
function setUp() public override {
|
|
40
41
|
super.setUpWithBlockNumber(29585474);
|
|
42
|
+
|
|
43
|
+
hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter), address(V4_POOL_MANAGER));
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
function _deployWithHook(address
|
|
46
|
+
function _deployWithHook(address _hook, bytes memory hookData, address currency) internal returns (address, bytes memory) {
|
|
44
47
|
bytes memory poolConfig = _generateDefaultPoolConfig(currency);
|
|
45
48
|
return
|
|
46
49
|
factory.deployWithHook(
|
|
@@ -51,7 +54,7 @@ contract HooksTest is BaseTest {
|
|
|
51
54
|
"TEST",
|
|
52
55
|
poolConfig,
|
|
53
56
|
users.platformReferrer,
|
|
54
|
-
|
|
57
|
+
_hook,
|
|
55
58
|
hookData
|
|
56
59
|
);
|
|
57
60
|
}
|
|
@@ -74,8 +77,6 @@ contract HooksTest is BaseTest {
|
|
|
74
77
|
|
|
75
78
|
vm.deal(users.creator, initialOrderSize);
|
|
76
79
|
|
|
77
|
-
BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
|
|
78
|
-
|
|
79
80
|
bytes memory hookData = _encodeExactInputSingle(
|
|
80
81
|
users.creator,
|
|
81
82
|
ISwapRouter.ExactInputSingleParams({
|
|
@@ -114,7 +115,7 @@ contract HooksTest is BaseTest {
|
|
|
114
115
|
assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function
|
|
118
|
+
function test_buySupplyWithEthUsingV4Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
|
|
118
119
|
vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
|
|
119
120
|
vm.assume(initialOrderSize < 1 ether);
|
|
120
121
|
|
|
@@ -122,7 +123,51 @@ contract HooksTest is BaseTest {
|
|
|
122
123
|
|
|
123
124
|
// lets try weth to usdc to zora
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
uint24 poolFee = 3000;
|
|
127
|
+
|
|
128
|
+
bytes memory hookData = _encodeExactInput(
|
|
129
|
+
users.creator,
|
|
130
|
+
ISwapRouter.ExactInputParams({
|
|
131
|
+
path: abi.encodePacked(address(weth), poolFee, USDC_ADDRESS, poolFee, zora),
|
|
132
|
+
recipient: address(hook),
|
|
133
|
+
amountIn: initialOrderSize,
|
|
134
|
+
amountOutMinimum: 0
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(zora);
|
|
139
|
+
|
|
140
|
+
vm.prank(users.creator);
|
|
141
|
+
(address coinAddress, bytes memory hookDataOut) = factory.deployWithHook{value: initialOrderSize}(
|
|
142
|
+
users.creator,
|
|
143
|
+
_getDefaultOwners(),
|
|
144
|
+
"https://test.com",
|
|
145
|
+
"Testcoin",
|
|
146
|
+
"TEST",
|
|
147
|
+
poolConfig,
|
|
148
|
+
users.platformReferrer,
|
|
149
|
+
address(hook),
|
|
150
|
+
hookData
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
coin = Coin(payable(coinAddress));
|
|
154
|
+
|
|
155
|
+
(uint256 amountCurrency, uint256 coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
|
|
156
|
+
|
|
157
|
+
assertEq(coin.currency(), zora, "currency");
|
|
158
|
+
assertGt(amountCurrency, 0, "amountCurrency > 0");
|
|
159
|
+
assertGt(coinsPurchased, 0, "coinsPurchased > 0");
|
|
160
|
+
assertEq(coin.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
|
|
161
|
+
// assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function test_buySupplyWithEthUsingV3Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
|
|
165
|
+
vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
|
|
166
|
+
vm.assume(initialOrderSize < 1 ether);
|
|
167
|
+
|
|
168
|
+
vm.deal(users.creator, initialOrderSize);
|
|
169
|
+
|
|
170
|
+
// lets try weth to usdc to zora
|
|
126
171
|
|
|
127
172
|
uint24 poolFee = 3000;
|
|
128
173
|
|
|
@@ -164,8 +209,6 @@ contract HooksTest is BaseTest {
|
|
|
164
209
|
function test_buySupplyWithEthUsingV3Hook_revertsWhenBadCall() public {
|
|
165
210
|
vm.deal(users.creator, 0.0001 ether);
|
|
166
211
|
|
|
167
|
-
BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
|
|
168
|
-
|
|
169
212
|
uint24 poolFee = 3000;
|
|
170
213
|
|
|
171
214
|
// exact output single is not supported
|
|
@@ -201,8 +244,6 @@ contract HooksTest is BaseTest {
|
|
|
201
244
|
uint256 initialOrderSize = 0.0001 ether;
|
|
202
245
|
vm.deal(users.creator, initialOrderSize);
|
|
203
246
|
|
|
204
|
-
BuySupplyWithSwapRouterHook hook = new BuySupplyWithSwapRouterHook(factory, address(swapRouter));
|
|
205
|
-
|
|
206
247
|
bytes memory hookData = _encodeExactInputSingle(
|
|
207
248
|
users.creator,
|
|
208
249
|
ISwapRouter.ExactInputSingleParams({
|