Skip to content

Commit

Permalink
Merge pull request #333 from whetstoneresearch/buncha-fixes
Browse files Browse the repository at this point in the history
Buncha fixes
  • Loading branch information
clemlak authored Feb 11, 2025
2 parents 5e45370 + 0050dff commit bf678a4
Show file tree
Hide file tree
Showing 23 changed files with 764 additions and 154 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ USING_ETH=FALSE
FEE=30
MAINNET_RPC_URL="http://..."
UNICHAIN_SEPOLIA_RPC_URL="https://sepolia.unichain.org"
PROTOCOL_OWNER=
V3_FEE=
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ IS_TOKEN_0=FALSE
USING_ETH=FALSE
FEE=30
```

### Deploy

First be sure to set the `PROTOCOL_OWNER` variable in your .env file. V1 contracts running on Uniswap V3 can be deployed using the following command:

```shell
# --rpc-url is the chain you want to deploy to
# --private-key is the deployer wallet (not the owner)
forge script ./script/V1DeploymentScript.s.sol --rpc-url https://... --private-key 0x... --broadcast
```
5 changes: 4 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ libs = ["lib"]
evm_version = "cancun"
via_ir = true
solc = '0.8.26'
fs_permissions = [{ access = "read", path = "./lib" }]
fs_permissions = [
{ access = "read", path = "./lib" },
{ access = "read", path = "./script" },
]
optimizer_runs = 0
bytecode_hash = "none"
optimizer = true
Expand Down
103 changes: 103 additions & 0 deletions script/V1DeploymentScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import { Script, console } from "forge-std/Script.sol";
import { Airlock, ModuleState } from "src/Airlock.sol";
import { TokenFactory } from "src/TokenFactory.sol";
import { GovernanceFactory } from "src/GovernanceFactory.sol";
import { UniswapV2Migrator, IUniswapV2Router02, IUniswapV2Factory } from "src/UniswapV2Migrator.sol";
import { UniswapV3Initializer, IUniswapV3Factory } from "src/UniswapV3Initializer.sol";

struct Addresses {
address uniswapV2Factory;
address uniswapV2Router02;
address uniswapV3Factory;
}

contract V1DeploymentScript is Script {
Airlock airlock;
TokenFactory tokenFactory;
UniswapV3Initializer uniswapV3Initializer;
GovernanceFactory governanceFactory;
UniswapV2Migrator uniswapV2LiquidityMigrator;

function run() public {
string memory path = "./script/addresses.json";
string memory json = vm.readFile(path);
bool exists = vm.keyExistsJson(json, string.concat(".", vm.toString(block.chainid)));
require(exists, string.concat("Missing Uniswap addresses for chain with id ", vm.toString(block.chainid)));

bytes memory data = vm.parseJson(json, string.concat(".", vm.toString(block.chainid)));
Addresses memory addresses = abi.decode(data, (Addresses));

console.log("uniswapV2Router02 %s", addresses.uniswapV2Router02);
console.log("uniswapV2Factory %s", addresses.uniswapV2Factory);
console.log("uniswapV3Factory %s", addresses.uniswapV3Factory);

address owner = vm.envOr("PROTOCOL_OWNER", address(0));
require(owner != address(0), "PROTOCOL_OWNER not set! Please edit your .env file.");
console.log(unicode"👑 PROTOCOL_OWNER set as %s", owner);

vm.startBroadcast();
console.log(unicode"🚀 Deploying contracts...");

// Owner of the protocol is set as the deployer to allow the whitelisting of modules, ownership
// is then transferred
airlock = new Airlock(msg.sender);
tokenFactory = new TokenFactory(address(airlock));
uniswapV3Initializer = new UniswapV3Initializer(address(airlock), IUniswapV3Factory(addresses.uniswapV3Factory));
governanceFactory = new GovernanceFactory(address(airlock));
uniswapV2LiquidityMigrator = new UniswapV2Migrator(
address(airlock),
IUniswapV2Factory(addresses.uniswapV2Factory),
IUniswapV2Router02(addresses.uniswapV2Router02),
owner
);

address[] memory modules = new address[](4);
modules[0] = address(tokenFactory);
modules[1] = address(uniswapV3Initializer);
modules[2] = address(governanceFactory);
modules[3] = address(uniswapV2LiquidityMigrator);

ModuleState[] memory states = new ModuleState[](4);
states[0] = ModuleState.TokenFactory;
states[1] = ModuleState.PoolInitializer;
states[2] = ModuleState.GovernanceFactory;
states[3] = ModuleState.LiquidityMigrator;

airlock.setModuleState(modules, states);

airlock.transferOwnership(owner);

console.log(unicode"✨ Contracts were successfully deployed:");
console.log("+----------------------------+--------------------------------------------+");
console.log("| Contract Name | Address |");
console.log("+----------------------------+--------------------------------------------+");
console.log("| Airlock | %s |", address(airlock));
console.log("+----------------------------+--------------------------------------------+");
console.log("| TokenFactory | %s |", address(tokenFactory));
console.log("+----------------------------+--------------------------------------------+");
console.log("| UniswapV3Initializer | %s |", address(uniswapV3Initializer));
console.log("+----------------------------+--------------------------------------------+");
console.log("| GovernanceFactory | %s |", address(governanceFactory));
console.log("+----------------------------+--------------------------------------------+");
console.log("| UniswapV2LiquidityMigrator | %s |", address(uniswapV2LiquidityMigrator));
console.log("+----------------------------+--------------------------------------------+");

vm.stopBroadcast();

// Some checks to ensure that the deployment was successful
for (uint256 i; i < modules.length; i++) {
require(airlock.getModuleState(modules[i]) == states[i], "Module state not set correctly");
}
require(address(tokenFactory.airlock()) == address(airlock), "TokenFactory not set correctly");
require(address(uniswapV3Initializer.airlock()) == address(airlock), "UniswapV3Initializer not set correctly");
require(address(governanceFactory.airlock()) == address(airlock), "GovernanceFactory not set correctly");
require(
address(uniswapV2LiquidityMigrator.airlock()) == address(airlock),
"UniswapV2LiquidityMigrator not set correctly"
);
require(airlock.owner() == owner, "Ownership not transferred to PROTOCOL_OWNER");
}
}
7 changes: 7 additions & 0 deletions script/addresses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"1301": {
"uniswap_v2_factory": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
"uniswap_v2_router_02": "0x920b806E40A00E02E7D2b94fFc89860fDaEd3640",
"uniswap_v3_factory": "0x1F98431c8aD98523631AE4a59f267346ea31F984"
}
}
85 changes: 46 additions & 39 deletions src/Airlock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ contract Airlock is Ownable {

mapping(address module => ModuleState state) public getModuleState;
mapping(address asset => AssetData data) public getAssetData;
mapping(address token => uint256 amount) public protocolFees;
mapping(address integrator => mapping(address token => uint256 amount)) public integratorFees;
mapping(address token => uint256 amount) public getProtocolFees;
mapping(address integrator => mapping(address token => uint256 amount)) public getIntegratorFees;

receive() external payable { }

Expand Down Expand Up @@ -151,6 +151,7 @@ contract Airlock is Ownable {

migrationPool =
createData.liquidityMigrator.initialize(asset, createData.numeraire, createData.liquidityMigratorData);
DERC20(asset).lockPool(migrationPool);

uint256 excessAsset = ERC20(asset).balanceOf(address(this));

Expand All @@ -168,14 +169,16 @@ contract Airlock is Ownable {
migrationPool: migrationPool,
numTokensToSell: createData.numTokensToSell,
totalSupply: createData.initialSupply,
integrator: createData.integrator
integrator: createData.integrator == address(0) ? owner() : createData.integrator
});

emit Create(asset, createData.numeraire, address(createData.poolInitializer), pool);
}

/**
* @notice Triggers the migration from one liquidity pool to another
* @notice Triggers the migration from the initial liquidity pool to the next one
* @dev Since anyone can call this function, the conditions for the migration are checked by the
* `poolInitializer` contract
* @param asset Address of the token to migrate
*/
function migrate(
Expand All @@ -196,51 +199,50 @@ contract Airlock is Ownable {
uint128 balance1
) = assetData.poolInitializer.exitLiquidity(assetData.pool);

uint256 protocolLpFees0 = fees0 * 5 / 100;
uint256 protocolLpFees1 = fees1 * 5 / 100;
_handleFees(token0, assetData.integrator, balance0, fees0);
_handleFees(token1, assetData.integrator, balance1, fees1);

// uint256 protocolProceedsFees0 = fees0 > 0 ? (balance0 - fees0) / 1000 : 0;
// uint256 protocolProceedsFees1 = fees1 > 0 ? (balance1 - fees1) / 1000 : 0;
// TODO: FIX PROTOCOL FEE CALCULATION
uint256 protocolProceedsFees0 = 0;
uint256 protocolProceedsFees1 = 0;

uint256 protocolFees0 = Math.max(protocolLpFees0, protocolProceedsFees0);
uint256 protocolFees1 = Math.max(protocolLpFees1, protocolProceedsFees1);

uint256 maxProtocolFees0 = fees0 * 20 / 100;
uint256 integratorFees0;
(integratorFees0, protocolFees0) = protocolFees0 > maxProtocolFees0
? (fees0 - maxProtocolFees0, maxProtocolFees0)
: (fees0 - protocolFees0, protocolFees0);

uint256 maxProtocolFees1 = fees1 * 20 / 100;
uint256 integratorFees1;
(integratorFees1, protocolFees1) = protocolFees1 > maxProtocolFees1
? (fees1 - maxProtocolFees1, maxProtocolFees1)
: (fees1 - protocolFees1, protocolFees1);

protocolFees[token0] += protocolFees0;
protocolFees[token1] += protocolFees1;
integratorFees[assetData.integrator][token0] += integratorFees0;
integratorFees[assetData.integrator][token1] += integratorFees1;

uint256 total0 = balance0 - fees0;
uint256 total1 = balance1 - fees1;
address liquidityMigrator = address(assetData.liquidityMigrator);

if (token0 == address(0)) {
SafeTransferLib.safeTransferETH(address(assetData.liquidityMigrator), total0);
SafeTransferLib.safeTransferETH(liquidityMigrator, balance0 - fees0);
} else {
ERC20(token0).safeTransfer(address(assetData.liquidityMigrator), total0);
ERC20(token0).safeTransfer(liquidityMigrator, balance0 - fees0);
}

ERC20(token1).safeTransfer(address(assetData.liquidityMigrator), total1);
ERC20(token1).safeTransfer(liquidityMigrator, balance1 - fees1);

assetData.liquidityMigrator.migrate(sqrtPriceX96, token0, token1, assetData.timelock);

emit Migrate(asset, assetData.pool);
}

/**
* @dev Computes and stores the protocol and integrators fees. Protocol fees are either 5% of the
* trading fees or 0.1% of the proceeds (token balance excluding fees) capped at a maximum of 20%
* of the trading fees
* @param token Address of the token to handle fees from
* @param integrator Address of the integrator to handle fees from
* @param balance Balance of the token including fees
* @param fees Trading fees
*/
function _handleFees(address token, address integrator, uint256 balance, uint256 fees) internal {
if (fees > 0) {
uint256 protocolLpFees = fees / 20;
uint256 protocolProceedsFees = (balance - fees) / 1000;
uint256 protocolFees = Math.max(protocolLpFees, protocolProceedsFees);
uint256 maxProtocolFees = fees / 5;
uint256 integratorFees;

(integratorFees, protocolFees) = protocolFees > maxProtocolFees
? (fees - maxProtocolFees, maxProtocolFees)
: (fees - protocolFees, protocolFees);

getProtocolFees[token] += protocolFees;
getIntegratorFees[integrator][token] += integratorFees;
}
}

/**
* @notice Sets the state of the givens modules
* @param modules Array of module addresses
Expand All @@ -266,7 +268,7 @@ contract Airlock is Ownable {
* @param amount Amount of fees to collect
*/
function collectProtocolFees(address to, address token, uint256 amount) external onlyOwner {
protocolFees[token] -= amount;
getProtocolFees[token] -= amount;

if (token == address(0)) {
SafeTransferLib.safeTransferETH(to, amount);
Expand All @@ -284,7 +286,7 @@ contract Airlock is Ownable {
* @param amount Amount of fees to collect
*/
function collectIntegratorFees(address to, address token, uint256 amount) external {
integratorFees[msg.sender][token] -= amount;
getIntegratorFees[msg.sender][token] -= amount;

if (token == address(0)) {
SafeTransferLib.safeTransferETH(to, amount);
Expand All @@ -295,6 +297,11 @@ contract Airlock is Ownable {
emit Collect(to, token, amount);
}

/**
* @dev Validates the state of a module
* @param module Address of the module
* @param state Expected state of the module
*/
function _validateModuleState(address module, ModuleState state) internal view {
require(getModuleState[address(module)] == state, WrongModuleState(module, state, getModuleState[module]));
}
Expand Down
9 changes: 9 additions & 0 deletions src/DERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ uint256 constant MAX_TOTAL_PRE_MINT_WAD = 0.1 ether;
/// @dev Maximum amount of tokens that can be minted in a year (% expressed in WAD)
uint256 constant MAX_YEARLY_MINT_RATE_WAD = 0.02 ether;

/// @dev Address of the canonical Permit2 contract
address constant PERMIT_2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

/**
* @notice Vesting data for a specific address
* @param totalAmount Total amount of vested tokens
Expand Down Expand Up @@ -268,6 +271,12 @@ contract DERC20 is ERC20, ERC20Votes, ERC20Permit, Ownable {
return super.nonces(owner_);
}

/// @inheritdoc ERC20
function allowance(address owner, address spender) public view override returns (uint256) {
if (spender == PERMIT_2) return type(uint256).max;
return super.allowance(owner, spender);
}

/// @inheritdoc ERC20
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
if (to == pool && isPoolUnlocked == false) revert PoolLocked();
Expand Down
7 changes: 5 additions & 2 deletions src/Governance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ contract Governance is
constructor(
string memory name_,
IVotes _token,
TimelockController _timelock
TimelockController _timelock,
uint48 initialVotingDelay,
uint32 initialVotingPeriod,
uint256 initialProposalThreshold
)
Governor(name_)
GovernorSettings(7200, 50_400, 0)
GovernorSettings(initialVotingDelay, initialVotingPeriod, initialProposalThreshold)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4)
GovernorTimelockControl(_timelock)
Expand Down
15 changes: 12 additions & 3 deletions src/GovernanceFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,20 @@ contract GovernanceFactory is IGovernanceFactory, ImmutableAirlock {
}

function create(address asset, bytes calldata data) external onlyAirlock returns (address, address) {
(string memory name) = abi.decode(data, (string));
(string memory name, uint48 initialVotingDelay, uint32 initialVotingPeriod, uint256 initialProposalThreshold) =
abi.decode(data, (string, uint48, uint32, uint256));

TimelockController timelockController = timelockFactory.create();
address governance =
address(new Governance(string.concat(name, " Governance"), IVotes(asset), timelockController));
address governance = address(
new Governance(
string.concat(name, " Governance"),
IVotes(asset),
timelockController,
initialVotingDelay,
initialVotingPeriod,
initialProposalThreshold
)
);
timelockController.grantRole(keccak256("PROPOSER_ROLE"), governance);
timelockController.grantRole(keccak256("CANCELLER_ROLE"), governance);
timelockController.grantRole(keccak256("EXECUTOR_ROLE"), address(0));
Expand Down
Loading

0 comments on commit bf678a4

Please sign in to comment.