-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/airlock (WIP) #53
Changes from 75 commits
8131f40
ada4347
3aabf45
02e2193
5ec5d1f
52eca8b
6288a83
632a5c8
0286c57
32789c5
9f6c08d
842bac0
f71f2c0
b2171ed
41b6bdb
11e413f
a21169b
9f7f061
470bef0
17e887e
561765d
5cd302b
9d8b90b
21f3826
9e14a6b
ca00fab
199f8c8
e137c82
7bf27ee
24e85fc
233e8de
0429375
85fea52
11308ef
11855d3
ea1e4ab
ec1af55
3d69808
7e989ca
f479a3a
c3bbf30
6d34d56
74992ea
aa87065
c1d1672
c1ca9ae
9819956
1185d47
d166531
fcdd062
0a4fc0c
3a507ae
a5c404b
ca3df6f
16a1ef2
4ffeaaf
57d4e41
04ab1b9
bce7f4c
b8bec22
c45fa02
a590f60
1484adb
e39a918
880aac8
bae1935
bbbe325
4043a99
cfbb426
eedbb0a
9c0830f
b14a2e5
859101f
3231b1a
ff2ea28
f550358
2a08414
95daaaf
1523381
8654a00
3426fad
8ae52f6
7f85209
0924ea9
cfa5676
8162f9b
691656c
77e0cc8
9b247f4
caa5836
3869fde
7010d5f
a139ed6
41ddda5
8c0f28e
c3a1b81
f8cafe7
b21b395
b1c0a36
f56da8e
7057a57
4121091
540f65a
0125203
959e623
47f78d2
6098e67
f200990
6e11efd
e0977d2
d653deb
57d751d
4b5a453
e0ef9ca
1eef929
b34b6a9
b7e4fd4
dde5675
f5e3744
96feb40
b59592b
0af001c
f627fd8
fe9b520
96526b2
7e0d115
df39733
d605fb3
fa55916
ef6f2f7
8ac5c2f
8e47c10
633eb7e
05ee04f
33df61a
3c82ea0
a3641d6
8864fe7
e8e9b9e
c8e2d99
6a13852
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,13 @@ | ||
v4-core/=lib/v4-periphery/lib/v4-core/ | ||
solady/=lib/solady/src/ | ||
@ensdomains/=lib/v4-core/node_modules/@ensdomains/ | ||
@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/ | ||
@openzeppelin/contracts/=lib/v4-core/lib/openzeppelin-contracts/contracts/ | ||
@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/contracts/ | ||
@uniswap/v4-core/=lib/v4-periphery/lib/v4-core/ | ||
ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/ | ||
erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/ | ||
forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/ | ||
forge-std/=lib/forge-std/src/ | ||
hardhat/=lib/v4-core/node_modules/hardhat/ | ||
openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/ | ||
permit2/=lib/v4-periphery/lib/permit2/ | ||
solmate/=lib/v4-core/lib/solmate/ | ||
v4-periphery/=lib/v4-periphery/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import {IPoolManager, PoolKey, Currency, IHooks, TickMath} from "v4-core/src/PoolManager.sol"; | ||
import {Ownable} from "@openzeppelin/access/Ownable.sol"; | ||
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; | ||
import {ITokenFactory} from "src/interfaces/ITokenFactory.sol"; | ||
import {IGovernanceFactory} from "src/interfaces/IGovernanceFactory.sol"; | ||
import {IHookFactory} from "src/interfaces/IHookFactory.sol"; | ||
|
||
enum FactoryState { | ||
NotWhitelisted, | ||
TokenFactory, | ||
GovernanceFactory, | ||
HookFactory | ||
} | ||
|
||
error WrongFactoryState(); | ||
|
||
struct TokenData { | ||
address governance; | ||
address hook; | ||
address[] recipients; | ||
uint256[] amounts; | ||
} | ||
|
||
event Create(address asset, address indexed numeraire, address governance, address hook); | ||
|
||
contract Airlock is Ownable { | ||
IPoolManager public immutable poolManager; | ||
|
||
mapping(address => FactoryState) public getFactoryState; | ||
mapping(address token => TokenData) public getTokenData; | ||
|
||
constructor(IPoolManager poolManager_) Ownable(msg.sender) { | ||
poolManager = poolManager_; | ||
} | ||
|
||
/** | ||
* TODO: | ||
* - Creating a token should incur fees (platform and frontend fees) | ||
* | ||
* @param tokenFactory Address of the factory contract deploying the ERC20 token | ||
* @param governanceFactory Address of the factory contract deploying the governance | ||
* @param hookFactory Address of the factory contract deploying the Uniswap v4 hook | ||
*/ | ||
function create( | ||
string memory name, | ||
string memory symbol, | ||
uint256 initialSupply, | ||
uint256 minimumProceeds, | ||
uint256 maximumProceeds, | ||
uint256 startingTime, | ||
uint256 endingTime, | ||
int24 minTick, | ||
int24 maxTick, | ||
uint256 epochLength, | ||
int24 gamma, | ||
address numeraire, | ||
address owner, | ||
address tokenFactory, | ||
bytes memory tokenData, | ||
address governanceFactory, | ||
bytes memory governanceData, | ||
address hookFactory, | ||
bytes memory hookData, | ||
address[] memory recipients, | ||
uint256[] memory amounts | ||
) external returns (address, address, address) { | ||
require(getFactoryState[tokenFactory] == FactoryState.TokenFactory, WrongFactoryState()); | ||
require(getFactoryState[governanceFactory] == FactoryState.GovernanceFactory, WrongFactoryState()); | ||
require(getFactoryState[hookFactory] == FactoryState.HookFactory, WrongFactoryState()); | ||
|
||
address token = | ||
ITokenFactory(tokenFactory).create(name, symbol, initialSupply, address(this), address(this), tokenData); | ||
|
||
bool isToken0 = token < numeraire ? true : false; | ||
|
||
// FIXME: We might want to double compare the minted / predicted addresses? | ||
(address predictedHook, bytes32 salt) = IHookFactory(hookFactory).predict( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be called offchain and salt passed in as a param? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit tricky because we don't know all the constructor parameters at first (for example |
||
poolManager, | ||
initialSupply, | ||
minimumProceeds, | ||
maximumProceeds, | ||
startingTime, | ||
endingTime, | ||
isToken0 ? minTick : maxTick, | ||
isToken0 ? maxTick : minTick, | ||
epochLength, | ||
gamma, | ||
isToken0, | ||
hookData | ||
); | ||
address hook = IHookFactory(hookFactory).create( | ||
poolManager, | ||
initialSupply, | ||
minimumProceeds, | ||
maximumProceeds, | ||
startingTime, | ||
endingTime, | ||
isToken0 ? minTick : maxTick, | ||
isToken0 ? maxTick : minTick, | ||
epochLength, | ||
gamma, | ||
isToken0, | ||
hookData, | ||
salt | ||
); | ||
ERC20(token).transfer(hook, initialSupply); | ||
|
||
(address governance, address timeLock) = | ||
IGovernanceFactory(governanceFactory).create(name, token, governanceData); | ||
// FIXME: I think the Timelock should be the owner of the token contract? | ||
Ownable(token).transferOwnership(timeLock); | ||
|
||
getTokenData[token] = TokenData({governance: governance, hook: hook, recipients: recipients, amounts: amounts}); | ||
|
||
PoolKey memory key = PoolKey({ | ||
currency0: Currency.wrap(isToken0 ? token : numeraire), | ||
currency1: Currency.wrap(isToken0 ? numeraire : token), | ||
fee: 0, // TODO: Do we want users to have the ability to set the fee? | ||
tickSpacing: 60, // TODO: Do we want users to have the ability to set the tickSpacing? | ||
hooks: IHooks(hook) | ||
}); | ||
|
||
poolManager.initialize(key, TickMath.getSqrtPriceAtTick(isToken0 ? minTick : maxTick), new bytes(0)); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah we haven't really added the events yet. If you have feedback for events that are helpful from a ux pov would you mind adding them to the sdk doc/making a quick issue? I can also help build context here if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added to #95 |
||
return (token, governance, hook); | ||
} | ||
|
||
/** | ||
* TODO: This function will be callable later by the hook contract itself, in order to move the liquidity | ||
* from the Uniswap v4 pool to a v2 pool. The flow would be something like: | ||
* 1) Enough tokens were sold to trigger the migration | ||
* 2) Hook contract will remove its positions | ||
*/ | ||
function migrate() external {} | ||
|
||
function setFactoryState(address factory, FactoryState state) external onlyOwner { | ||
getFactoryState[factory] = state; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; | ||
import {ERC20Votes} from "@openzeppelin/token/ERC20/extensions/ERC20Votes.sol"; | ||
import {EIP712} from "@openzeppelin/utils/cryptography/EIP712.sol"; | ||
import {Ownable} from "@openzeppelin/access/Ownable.sol"; | ||
import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; | ||
import {Nonces} from "@openzeppelin/utils/Nonces.sol"; | ||
|
||
/** | ||
* TODO: | ||
* - Add mint cap: bounded annual max inflation which can only go down | ||
*/ | ||
error MintingNotStartedYet(); | ||
|
||
contract DERC20 is ERC20Votes, ERC20Permit, Ownable { | ||
uint256 public immutable mintStartDate; | ||
uint256 public immutable yearlyMintCap; | ||
|
||
constructor(string memory name_, string memory symbol_, uint256 totalSupply_, address recipient, address owner_) | ||
ERC20(name_, symbol_) | ||
ERC20Permit(name_) | ||
Ownable(owner_) | ||
{ | ||
_mint(recipient, totalSupply_); | ||
mintStartDate = block.timestamp + 365 days; | ||
} | ||
|
||
function mint(address to, uint256 value) external onlyOwner { | ||
require(block.timestamp >= mintStartDate, MintingNotStartedYet()); | ||
_mint(to, value); | ||
} | ||
|
||
function clock() public view override returns (uint48) { | ||
return uint48(block.timestamp); | ||
} | ||
|
||
function CLOCK_MODE() public pure override returns (string memory) { | ||
return "mode=timestamp"; | ||
} | ||
|
||
function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { | ||
return super.nonces(owner); | ||
} | ||
|
||
function _update(address from, address to, uint256 value) internal virtual override(ERC20, ERC20Votes) { | ||
super._update(from, to, value); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; | ||
import {Hooks} from "v4-core/src/libraries/Hooks.sol"; | ||
import {IHookFactory} from "src/interfaces/IHookFactory.sol"; | ||
import {Doppler} from "src/Doppler.sol"; | ||
import {HookMiner} from "src/HookMiner.sol"; | ||
|
||
contract DopplerFactory is IHookFactory { | ||
function create( | ||
IPoolManager poolManager, | ||
uint256 numTokensToSell, | ||
uint256 startingTime, | ||
uint256 endingTime, | ||
uint256 minimumProceeds, | ||
uint256 maximumProceeds, | ||
int24 startingTick, | ||
int24 endingTick, | ||
uint256 epochLength, | ||
int24 gamma, | ||
bool isToken0, | ||
bytes memory, | ||
bytes32 salt | ||
) external returns (address) { | ||
return address( | ||
new Doppler{salt: salt}( | ||
poolManager, | ||
numTokensToSell, | ||
startingTime, | ||
endingTime, | ||
minimumProceeds, | ||
maximumProceeds, | ||
startingTick, | ||
endingTick, | ||
epochLength, | ||
gamma, | ||
isToken0, | ||
3 // numPDSlugs | ||
) | ||
); | ||
} | ||
|
||
function predict( | ||
IPoolManager poolManager, | ||
uint256 numTokensToSell, | ||
uint256 startingTime, | ||
uint256 endingTime, | ||
uint256 minimumProceeds, | ||
uint256 maximumProceeds, | ||
int24 startingTick, | ||
int24 endingTick, | ||
uint256 epochLength, | ||
int24 gamma, | ||
bool isToken0, | ||
bytes memory | ||
) public view returns (address hookAddress, bytes32 salt) { | ||
(hookAddress, salt) = HookMiner.find( | ||
address(this), | ||
uint160( | ||
Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | ||
| Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | ||
), | ||
type(Doppler).creationCode, | ||
abi.encode( | ||
poolManager, | ||
numTokensToSell, | ||
startingTime, | ||
endingTime, | ||
minimumProceeds, | ||
maximumProceeds, | ||
startingTick, | ||
endingTick, | ||
epochLength, | ||
gamma, | ||
isToken0 | ||
) | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to include the
poolId
here? Wondering how an interface will get from aasset
address topoolId
and find the appropriate pool.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
poolId
is included, probably need to include inTokenData
above as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add it 👌