-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #53 from whetstoneresearch/feat/airlock
Feat/airlock (WIP)
- Loading branch information
Showing
21 changed files
with
1,164 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# Airlock | ||
|
||
## Open Questions | ||
|
||
- 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. | ||
|
||
## Architecture | ||
|
||
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: | ||
|
||
_Note: a "module" must be whitelisted before it can be used._ | ||
|
||
| Module | Role | | ||
| ----------------- | ----------------------------------------- | | ||
| TokenFactory | Deploys asset tokens | | ||
| GovernanceFactory | Deploys governance and timelock contracts | | ||
| HookFactory | Deploys hook contracts | | ||
| Migrator | Migrates tokens to a new hook | | ||
|
||
## Initialization | ||
|
||
This sequence diagram describes the initialization of a new Doppler hook, along with the deployment of the related token: | ||
|
||
```mermaid | ||
sequenceDiagram | ||
participant U as Creator | ||
participant A as Airlock | ||
participant F as TokenFactory | ||
participant P as PoolManager | ||
participant H as Hook | ||
U->>A: `create()` | ||
A->>F: `deploy()` | ||
F-->>A: send tokens | ||
A->>P: `initialize()` | ||
P->>H: `beforeInitialize()` | ||
H-->>A: take tokens | ||
H->>P: `unlock()` | ||
P->>+H: `unlockCallback()` | ||
H->>P: `modifyLiquidity()` | ||
H->>P: ... | ||
H->>P: `sync()` | ||
H->>-P: `settle()` | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import {IPoolManager, PoolKey, Currency, 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, IHook} from "src/interfaces/IHookFactory.sol"; | ||
import {IMigrator} from "src/interfaces/IMigrator.sol"; | ||
|
||
enum ModuleState { | ||
NotWhitelisted, | ||
TokenFactory, | ||
GovernanceFactory, | ||
HookFactory, | ||
Migrator | ||
} | ||
|
||
error WrongModuleState(); | ||
|
||
error WrongInitialSupply(); | ||
|
||
error ArrayLengthsMismatch(); | ||
|
||
struct TokenData { | ||
PoolKey poolKey; | ||
address timelock; | ||
address governance; | ||
IMigrator migrator; | ||
address[] recipients; | ||
uint256[] amounts; | ||
} | ||
|
||
event Create(address asset, PoolKey poolKey, address hook); | ||
|
||
event Migrate(address asset, address pool); | ||
|
||
event SetModuleState(address module, ModuleState state); | ||
|
||
contract Airlock is Ownable { | ||
IPoolManager public immutable poolManager; | ||
|
||
mapping(address => ModuleState) public getModuleState; | ||
mapping(address token => TokenData) public getTokenData; | ||
|
||
receive() external payable {} | ||
|
||
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 numTokensToSell, | ||
PoolKey memory poolKey, | ||
address owner, | ||
address[] memory recipients, | ||
uint256[] memory amounts, | ||
ITokenFactory tokenFactory, | ||
bytes memory tokenData, | ||
IGovernanceFactory governanceFactory, | ||
bytes memory governanceData, | ||
IHookFactory hookFactory, | ||
bytes memory hookData, | ||
IMigrator migrator, | ||
bytes32 salt | ||
) external returns (address, address, address) { | ||
require(getModuleState[address(tokenFactory)] == ModuleState.TokenFactory, WrongModuleState()); | ||
require(getModuleState[address(governanceFactory)] == ModuleState.GovernanceFactory, WrongModuleState()); | ||
require(getModuleState[address(hookFactory)] == ModuleState.HookFactory, WrongModuleState()); | ||
require(getModuleState[address(migrator)] == ModuleState.Migrator, WrongModuleState()); | ||
|
||
require(recipients.length == amounts.length, ArrayLengthsMismatch()); | ||
|
||
uint256 totalToMint = numTokensToSell; | ||
for (uint256 i; i < amounts.length; i++) { | ||
totalToMint += amounts[i]; | ||
} | ||
require(totalToMint == initialSupply, WrongInitialSupply()); | ||
|
||
address token = tokenFactory.create(name, symbol, initialSupply, address(this), address(this), tokenData, salt); | ||
address hook = hookFactory.create(poolManager, numTokensToSell, hookData, salt); | ||
|
||
ERC20(token).transfer(hook, numTokensToSell); | ||
|
||
// TODO: I don't think we need to pass the salt here, create2 is not needed anyway. | ||
(address governance, address timelock) = governanceFactory.create(name, token, governanceData); | ||
Ownable(token).transferOwnership(timelock); | ||
|
||
getTokenData[token] = TokenData({ | ||
governance: governance, | ||
recipients: recipients, | ||
amounts: amounts, | ||
migrator: migrator, | ||
timelock: timelock, | ||
poolKey: poolKey | ||
}); | ||
|
||
// TODO: Do we really have to initialize the pool at the right price? | ||
poolManager.initialize(poolKey, TickMath.getSqrtPriceAtTick(0), new bytes(0)); | ||
|
||
emit Create(token, poolKey, hook); | ||
|
||
return (token, governance, hook); | ||
} | ||
|
||
function migrate(address asset) external { | ||
TokenData memory tokenData = getTokenData[asset]; | ||
|
||
uint256 length = tokenData.recipients.length; | ||
for (uint256 i; i < length; i++) { | ||
ERC20(asset).transfer(tokenData.recipients[i], tokenData.amounts[i]); | ||
} | ||
|
||
(uint256 amount0, uint256 amount1) = IHook(address(tokenData.poolKey.hooks)).migrate(); | ||
|
||
address currency0 = Currency.unwrap(tokenData.poolKey.currency0); | ||
address currency1 = Currency.unwrap(tokenData.poolKey.currency1); | ||
|
||
if (currency0 != address(0)) ERC20(currency0).transfer(address(tokenData.migrator), amount0); | ||
ERC20(currency1).transfer(address(tokenData.migrator), amount1); | ||
|
||
(address pool,) = tokenData.migrator.migrate{value: currency0 == address(0) ? amount0 : 0}( | ||
currency0, currency1, amount0, amount1, tokenData.timelock, new bytes(0) | ||
); | ||
|
||
emit Migrate(asset, pool); | ||
} | ||
|
||
// TODO: Maybe we should accept arrays here to batch update states? | ||
function setModuleState(address module, ModuleState state) external onlyOwner { | ||
getModuleState[module] = state; | ||
emit SetModuleState(module, state); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/// 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 {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 ERC20, ERC20Votes, ERC20Permit, Ownable { | ||
uint256 public immutable mintStartDate; | ||
uint256 public immutable yearlyMintCap; | ||
|
||
constructor(string memory name_, string memory symbol_, uint256 initialSupply, address recipient, address owner_) | ||
ERC20(name_, symbol_) | ||
ERC20Permit(name_) | ||
Ownable(owner_) | ||
{ | ||
_mint(recipient, initialSupply); | ||
mintStartDate = block.timestamp + 365 days; | ||
} | ||
|
||
function mint(address to, uint256 value) external onlyOwner { | ||
require(block.timestamp >= mintStartDate, MintingNotStartedYet()); | ||
_mint(to, value); | ||
} | ||
|
||
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) { | ||
super._update(from, to, value); | ||
} | ||
|
||
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) { | ||
return super.nonces(owner); | ||
} | ||
} |
Oops, something went wrong.