Skip to content

Commit

Permalink
Merge pull request #53 from whetstoneresearch/feat/airlock
Browse files Browse the repository at this point in the history
Feat/airlock (WIP)
  • Loading branch information
clemlak authored Nov 4, 2024
2 parents 0092a2d + 6a13852 commit 0d6fc58
Show file tree
Hide file tree
Showing 21 changed files with 1,164 additions and 67 deletions.
44 changes: 44 additions & 0 deletions Airlock.md
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()`
```
4 changes: 1 addition & 3 deletions remappings.txt
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/
146 changes: 146 additions & 0 deletions src/Airlock.sol
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);
}
}
41 changes: 41 additions & 0 deletions src/DERC20.sol
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);
}
}
Loading

0 comments on commit 0d6fc58

Please sign in to comment.