Skip to content

Commit 0d6fc58

Browse files
authored
Merge pull request #53 from whetstoneresearch/feat/airlock
Feat/airlock (WIP)
2 parents 0092a2d + 6a13852 commit 0d6fc58

21 files changed

+1164
-67
lines changed

Airlock.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Airlock
2+
3+
## Open Questions
4+
5+
- Should we premint the team allocation or not? If we don't premint it, then we'll have to mint in the migrate function, distribute the tokens and then transfer the ownership to the Timelock contract.
6+
7+
## Architecture
8+
9+
The Airlock contract takes care of the deployment of new tokens along with the initialization of the related hook and governance contracts. The system is flexible and can support different "modules" as long as they implement the required interfaces:
10+
11+
_Note: a "module" must be whitelisted before it can be used._
12+
13+
| Module | Role |
14+
| ----------------- | ----------------------------------------- |
15+
| TokenFactory | Deploys asset tokens |
16+
| GovernanceFactory | Deploys governance and timelock contracts |
17+
| HookFactory | Deploys hook contracts |
18+
| Migrator | Migrates tokens to a new hook |
19+
20+
## Initialization
21+
22+
This sequence diagram describes the initialization of a new Doppler hook, along with the deployment of the related token:
23+
24+
```mermaid
25+
sequenceDiagram
26+
participant U as Creator
27+
participant A as Airlock
28+
participant F as TokenFactory
29+
participant P as PoolManager
30+
participant H as Hook
31+
32+
U->>A: `create()`
33+
A->>F: `deploy()`
34+
F-->>A: send tokens
35+
A->>P: `initialize()`
36+
P->>H: `beforeInitialize()`
37+
H-->>A: take tokens
38+
H->>P: `unlock()`
39+
P->>+H: `unlockCallback()`
40+
H->>P: `modifyLiquidity()`
41+
H->>P: ...
42+
H->>P: `sync()`
43+
H->>-P: `settle()`
44+
```

remappings.txt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
v4-core/=lib/v4-periphery/lib/v4-core/
22
solady/=lib/solady/src/
33
@ensdomains/=lib/v4-core/node_modules/@ensdomains/
4-
@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/
5-
@openzeppelin/contracts/=lib/v4-core/lib/openzeppelin-contracts/contracts/
4+
@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/contracts/
65
@uniswap/v4-core/=lib/v4-periphery/lib/v4-core/
76
ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/
87
erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/
98
forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/
109
forge-std/=lib/forge-std/src/
1110
hardhat/=lib/v4-core/node_modules/hardhat/
12-
openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/
1311
permit2/=lib/v4-periphery/lib/permit2/
1412
solmate/=lib/v4-core/lib/solmate/
1513
v4-periphery/=lib/v4-periphery/

src/Airlock.sol

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {IPoolManager, PoolKey, Currency, TickMath} from "v4-core/src/PoolManager.sol";
5+
import {Ownable} from "@openzeppelin/access/Ownable.sol";
6+
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
7+
import {ITokenFactory} from "src/interfaces/ITokenFactory.sol";
8+
import {IGovernanceFactory} from "src/interfaces/IGovernanceFactory.sol";
9+
import {IHookFactory, IHook} from "src/interfaces/IHookFactory.sol";
10+
import {IMigrator} from "src/interfaces/IMigrator.sol";
11+
12+
enum ModuleState {
13+
NotWhitelisted,
14+
TokenFactory,
15+
GovernanceFactory,
16+
HookFactory,
17+
Migrator
18+
}
19+
20+
error WrongModuleState();
21+
22+
error WrongInitialSupply();
23+
24+
error ArrayLengthsMismatch();
25+
26+
struct TokenData {
27+
PoolKey poolKey;
28+
address timelock;
29+
address governance;
30+
IMigrator migrator;
31+
address[] recipients;
32+
uint256[] amounts;
33+
}
34+
35+
event Create(address asset, PoolKey poolKey, address hook);
36+
37+
event Migrate(address asset, address pool);
38+
39+
event SetModuleState(address module, ModuleState state);
40+
41+
contract Airlock is Ownable {
42+
IPoolManager public immutable poolManager;
43+
44+
mapping(address => ModuleState) public getModuleState;
45+
mapping(address token => TokenData) public getTokenData;
46+
47+
receive() external payable {}
48+
49+
constructor(IPoolManager poolManager_) Ownable(msg.sender) {
50+
poolManager = poolManager_;
51+
}
52+
53+
/**
54+
* TODO:
55+
* - Creating a token should incur fees (platform and frontend fees)
56+
*
57+
* @param tokenFactory Address of the factory contract deploying the ERC20 token
58+
* @param governanceFactory Address of the factory contract deploying the governance
59+
* @param hookFactory Address of the factory contract deploying the Uniswap v4 hook
60+
*/
61+
function create(
62+
string memory name,
63+
string memory symbol,
64+
uint256 initialSupply,
65+
uint256 numTokensToSell,
66+
PoolKey memory poolKey,
67+
address owner,
68+
address[] memory recipients,
69+
uint256[] memory amounts,
70+
ITokenFactory tokenFactory,
71+
bytes memory tokenData,
72+
IGovernanceFactory governanceFactory,
73+
bytes memory governanceData,
74+
IHookFactory hookFactory,
75+
bytes memory hookData,
76+
IMigrator migrator,
77+
bytes32 salt
78+
) external returns (address, address, address) {
79+
require(getModuleState[address(tokenFactory)] == ModuleState.TokenFactory, WrongModuleState());
80+
require(getModuleState[address(governanceFactory)] == ModuleState.GovernanceFactory, WrongModuleState());
81+
require(getModuleState[address(hookFactory)] == ModuleState.HookFactory, WrongModuleState());
82+
require(getModuleState[address(migrator)] == ModuleState.Migrator, WrongModuleState());
83+
84+
require(recipients.length == amounts.length, ArrayLengthsMismatch());
85+
86+
uint256 totalToMint = numTokensToSell;
87+
for (uint256 i; i < amounts.length; i++) {
88+
totalToMint += amounts[i];
89+
}
90+
require(totalToMint == initialSupply, WrongInitialSupply());
91+
92+
address token = tokenFactory.create(name, symbol, initialSupply, address(this), address(this), tokenData, salt);
93+
address hook = hookFactory.create(poolManager, numTokensToSell, hookData, salt);
94+
95+
ERC20(token).transfer(hook, numTokensToSell);
96+
97+
// TODO: I don't think we need to pass the salt here, create2 is not needed anyway.
98+
(address governance, address timelock) = governanceFactory.create(name, token, governanceData);
99+
Ownable(token).transferOwnership(timelock);
100+
101+
getTokenData[token] = TokenData({
102+
governance: governance,
103+
recipients: recipients,
104+
amounts: amounts,
105+
migrator: migrator,
106+
timelock: timelock,
107+
poolKey: poolKey
108+
});
109+
110+
// TODO: Do we really have to initialize the pool at the right price?
111+
poolManager.initialize(poolKey, TickMath.getSqrtPriceAtTick(0), new bytes(0));
112+
113+
emit Create(token, poolKey, hook);
114+
115+
return (token, governance, hook);
116+
}
117+
118+
function migrate(address asset) external {
119+
TokenData memory tokenData = getTokenData[asset];
120+
121+
uint256 length = tokenData.recipients.length;
122+
for (uint256 i; i < length; i++) {
123+
ERC20(asset).transfer(tokenData.recipients[i], tokenData.amounts[i]);
124+
}
125+
126+
(uint256 amount0, uint256 amount1) = IHook(address(tokenData.poolKey.hooks)).migrate();
127+
128+
address currency0 = Currency.unwrap(tokenData.poolKey.currency0);
129+
address currency1 = Currency.unwrap(tokenData.poolKey.currency1);
130+
131+
if (currency0 != address(0)) ERC20(currency0).transfer(address(tokenData.migrator), amount0);
132+
ERC20(currency1).transfer(address(tokenData.migrator), amount1);
133+
134+
(address pool,) = tokenData.migrator.migrate{value: currency0 == address(0) ? amount0 : 0}(
135+
currency0, currency1, amount0, amount1, tokenData.timelock, new bytes(0)
136+
);
137+
138+
emit Migrate(asset, pool);
139+
}
140+
141+
// TODO: Maybe we should accept arrays here to batch update states?
142+
function setModuleState(address module, ModuleState state) external onlyOwner {
143+
getModuleState[module] = state;
144+
emit SetModuleState(module, state);
145+
}
146+
}

src/DERC20.sol

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
5+
import {ERC20Votes} from "@openzeppelin/token/ERC20/extensions/ERC20Votes.sol";
6+
import {Ownable} from "@openzeppelin/access/Ownable.sol";
7+
import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
8+
import {Nonces} from "@openzeppelin/utils/Nonces.sol";
9+
10+
/**
11+
* TODO:
12+
* - Add mint cap: bounded annual max inflation which can only go down
13+
*/
14+
error MintingNotStartedYet();
15+
16+
contract DERC20 is ERC20, ERC20Votes, ERC20Permit, Ownable {
17+
uint256 public immutable mintStartDate;
18+
uint256 public immutable yearlyMintCap;
19+
20+
constructor(string memory name_, string memory symbol_, uint256 initialSupply, address recipient, address owner_)
21+
ERC20(name_, symbol_)
22+
ERC20Permit(name_)
23+
Ownable(owner_)
24+
{
25+
_mint(recipient, initialSupply);
26+
mintStartDate = block.timestamp + 365 days;
27+
}
28+
29+
function mint(address to, uint256 value) external onlyOwner {
30+
require(block.timestamp >= mintStartDate, MintingNotStartedYet());
31+
_mint(to, value);
32+
}
33+
34+
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
35+
super._update(from, to, value);
36+
}
37+
38+
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
39+
return super.nonces(owner);
40+
}
41+
}

0 commit comments

Comments
 (0)