387 lines
No EOL
14 KiB
Markdown
387 lines
No EOL
14 KiB
Markdown
## Foundry
|
|
|
|
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
|
|
|
|
Foundry consists of:
|
|
|
|
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
|
|
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
|
|
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
|
|
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
|
|
|
|
## Documentation
|
|
|
|
https://book.getfoundry.sh/
|
|
|
|
## Usage
|
|
|
|
### Install
|
|
|
|
```shell
|
|
$ git clone
|
|
$ git submodule init
|
|
$ git submodule update
|
|
$ cd lib/uni-v3-lib
|
|
$ yarn
|
|
```
|
|
|
|
### Build
|
|
|
|
```shell
|
|
$ forge build
|
|
```
|
|
|
|
### Test
|
|
|
|
```shell
|
|
$ forge test
|
|
```
|
|
|
|
### Format
|
|
|
|
```shell
|
|
$ forge fmt
|
|
```
|
|
|
|
### Gas Snapshots
|
|
|
|
```shell
|
|
$ forge snapshot
|
|
```
|
|
|
|
### Anvil
|
|
|
|
```shell
|
|
$ anvil
|
|
```
|
|
|
|
### Deploy
|
|
|
|
```shell
|
|
source .env
|
|
forge script script/Deploy.sol:SepoliaScript --slow --broadcast --verify --rpc-url ${SEPOLIA_RPC_URL}
|
|
```
|
|
|
|
if verification fails:
|
|
```shell
|
|
forge verify-contract --watch --chain sepolia --constructor-args $(cast abi-encode "constructor(string,string,address,address,address)" "Harberger Tax" "HARB" "0x0227628f3F023bb0B980b67D528571c95c6DaC1c" "0xb16F35c0Ae2912430DAc15764477E179D9B9EbEa" "0x64dda11815b883c589afed914666ef2d63c8c338") 0x7517db0f2b24223f2f0e3567149ca180e204da8a Harb
|
|
|
|
forge verify-contract --watch --chain sepolia --constructor-args $(cast abi-encode "constructor(address)" "0x7517db0f2b24223f2f0e3567149ca180e204da8a") 0x00b4d656b8182d0c2f4841b7a6f1429b94f73a66 Stake
|
|
```
|
|
|
|
|
|
|
|
### Cast
|
|
|
|
```shell
|
|
$ cast <subcommand>
|
|
```
|
|
|
|
### Help
|
|
|
|
```shell
|
|
$ forge --help
|
|
$ anvil --help
|
|
$ cast --help
|
|
```
|
|
|
|
|
|
## Deployment on Sepolia
|
|
|
|
### Harb
|
|
|
|
address: 0x7517db0f2b24223f2f0e3567149ca180e204da8a
|
|
|
|
[abi](../subgraph/harb/abis/Harb.json)
|
|
|
|
### Stake
|
|
|
|
address: 0xeB64dD1b4c0D59c9c8Cb3d9EA0E319cD0d45825f
|
|
|
|
[abi](../subgraph/harb/abis/Stake.json)
|
|
|
|
|
|
## References
|
|
|
|
- take percentage math from here: https://github.com/attestate/libharberger/tree/master
|
|
- implement this ERC for Harb: https://eips.ethereum.org/EIPS/eip-4907
|
|
- TaxHouse contract - erc721 owner - 20% of supply
|
|
- implement auction model with tax
|
|
- instrument holder is user in ERC4907
|
|
- 5% of supply founder and influencers
|
|
- direct ERC721 ownernership
|
|
- add this function: https://github.com/721labs/partial-common-ownership/blob/3e7713bc60b6bb2e103320036ec5aeaaaceb7d2b/contracts/token/modules/Taxation.sol#L260
|
|
- address this issue: "Seems like an owner could always frontrun buy attempts by increasing the valuation by one wei."
|
|
- rename TAX_FLOOR_DURATION to cooldown?
|
|
- limit discovery position growth to max_issuance / day
|
|
|
|
open features:
|
|
- token minting limit / limit on discovery position growth
|
|
- ERC721 & ERC4907, user/owner separation, influencer incentives
|
|
- token contract not visible in uniswap sepolia
|
|
- snatch collision
|
|
- previousTotalSupply at beginning?
|
|
- profit for staking position
|
|
- tax paid for staking position
|
|
- partially snatched
|
|
- liquidation bot
|
|
- shift/slide bot
|
|
- ubi claim bot
|
|
|
|
## old lp
|
|
|
|
```
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
|
|
import "@uniswap-v3-core/libraries/FixedPoint128.sol";
|
|
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
|
import "@aperture/uni-v3-lib/TickMath.sol";
|
|
import "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
|
import "@aperture/uni-v3-lib/PoolAddress.sol";
|
|
import "@aperture/uni-v3-lib/CallbackValidation.sol";
|
|
import "@openzeppelin/token/ERC20/IERC20.sol";
|
|
|
|
/**
|
|
* @title LiquidityManager - A contract that supports the harb ecosystem. It
|
|
* protects the communities liquidity while allowing a manager role to
|
|
* take strategic liqudity positions.
|
|
*/
|
|
contract LiquidityManager {
|
|
// default fee of 1%
|
|
uint24 constant FEE = uint24(10_000);
|
|
|
|
// the address of the Uniswap V3 factory
|
|
address immutable factory;
|
|
IWETH9 immutable weth;
|
|
Harb immutable harb;
|
|
IUniswapV3Pool immutable pool;
|
|
PoolKey immutable poolKey;
|
|
bool immutable token0isWeth;
|
|
|
|
|
|
struct TokenPosition {
|
|
// the liquidity of the position
|
|
uint128 liquidity;
|
|
uint128 ethOwed;
|
|
// the fee growth of the aggregate position as of the last action on the individual position
|
|
uint256 feeGrowthInside0LastX128;
|
|
uint256 feeGrowthInside1LastX128;
|
|
}
|
|
|
|
/// @dev The token ID position data
|
|
mapping(bytes26 => TokenPosition) private _positions;
|
|
|
|
modifier checkDeadline(uint256 deadline) {
|
|
require(block.timestamp <= deadline, "Transaction too old");
|
|
_;
|
|
}
|
|
|
|
/// @notice Emitted when liquidity is increased for a position
|
|
/// @param liquidity The amount by which liquidity for the NFT position was increased
|
|
/// @param amount0 The amount of token0 that was paid for the increase in liquidity
|
|
/// @param amount1 The amount of token1 that was paid for the increase in liquidity
|
|
event IncreaseLiquidity(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 amount0, uint256 amount1);
|
|
/// @notice Emitted when liquidity is decreased for a position
|
|
/// @param liquidity The amount by which liquidity for the NFT position was decreased
|
|
/// @param ethReceived The amount of WETH that was accounted for the decrease in liquidity
|
|
event PositionLiquidated(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 ethReceived);
|
|
|
|
constructor(address _factory, address _WETH9, address _harb) {
|
|
factory = _factory;
|
|
weth = IWETH9(_WETH9);
|
|
poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
|
|
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
|
|
harb = Harb(_harb);
|
|
token0isWeth = _WETH9 < _harb;
|
|
}
|
|
|
|
function posKey(int24 tickLower, int24 tickUpper) internal pure returns (bytes6 _posKey) {
|
|
bytes memory _posKeyBytes = abi.encodePacked(tickLower, tickUpper);
|
|
assembly {
|
|
_posKey := mload(add(_posKeyBytes, 6))
|
|
}
|
|
}
|
|
|
|
function positions(int24 tickLower, int24 tickUpper)
|
|
external
|
|
view
|
|
returns (uint128 liquidity, uint128 ethOwed, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128)
|
|
{
|
|
TokenPosition memory position = _positions[posKey(tickLower, tickUpper)];
|
|
return (position.liquidity, position.ethOwed, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128);
|
|
}
|
|
|
|
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external {
|
|
CallbackValidation.verifyCallback(factory, poolKey);
|
|
|
|
if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed);
|
|
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
|
|
}
|
|
|
|
|
|
/// @notice Add liquidity to an initialized pool
|
|
// TODO: use uint256 amount0Min; uint256 amount1Min; if addLiquidity is called directly
|
|
function addLiquidity(int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 deadline) internal checkDeadline(deadline) {
|
|
|
|
|
|
(uint256 amount0, uint256 amount1) = pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
|
|
// If addLiquidity is only called after other pool operations that have checked slippage, this here is not needed
|
|
//require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check");
|
|
|
|
|
|
// read position and start tracking in storage
|
|
bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper);
|
|
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
|
|
TokenPosition storage position = _positions[posKey(token, tickLower, tickUpper)];
|
|
if (liquidity == 0) {
|
|
// create entry
|
|
position = TokenPosition({
|
|
liquidity: liquidity,
|
|
ethOwed: 0,
|
|
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
|
|
feeGrowthInside1LastX128: feeGrowthInside1LastX128
|
|
});
|
|
} else {
|
|
position.ethOwed += FullMath.mulDiv(
|
|
(token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
|
position.liquidity,
|
|
FixedPoint128.Q128
|
|
);
|
|
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
|
|
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
|
|
position.liquidity += liquidity;
|
|
}
|
|
emit IncreaseLiquidity(tickLower, tickUpper, liquidity, amount0, amount1);
|
|
}
|
|
|
|
function liquidatePosition(int24 tickLower, int24 tickUpper, uint256 amount0Min, uint256 amount1Min)
|
|
internal
|
|
returns (uint256 ethReceived, uint256 liquidity)
|
|
{
|
|
// load position
|
|
TokenPosition storage position = _positions[posKey(tickLower, tickUpper)];
|
|
|
|
// burn and check slippage
|
|
uint256 liquidity = position.liquidity;
|
|
(uint256 amount0, uint256 amount1) = pool.burn(tickLower, tickUpper, liquidity);
|
|
require(amount0 >= amount0Min && amount1 >= amount1Min, "Price slippage check");
|
|
// TODO: send harb fees or burn?
|
|
//harb.burn(token0isWeth ? amount1 : amount0);
|
|
|
|
// calculate and transfer fees
|
|
bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper);
|
|
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
|
|
uint256 ethOwed = position.ethOwed;
|
|
ethOwed +=
|
|
FullMath.mulDiv(
|
|
(token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
|
liquidity,
|
|
FixedPoint128.Q128
|
|
);
|
|
weth.withdraw(ethOwed);
|
|
(bool sent, ) = feeRecipient.call{value: ethOwed}("");
|
|
require(sent, "Failed to send Ether");
|
|
|
|
// event, cleanup and return
|
|
ethReceived = token0isWeth ? amount0 - ethOwed : amount1 - ethOwed;
|
|
emit PositionLiquidated(tickLower, tickUpper, liquidity, ethReceived);
|
|
delete position.liquidity;
|
|
delete position.ethOwed;
|
|
delete position.feeGrowthInside0LastX128;
|
|
delete position.feeGrowthInside1LastX128;
|
|
}
|
|
|
|
// function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) {
|
|
// // Fetch the current sqrtPriceX96 from the pool
|
|
// (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0();
|
|
|
|
// // Convert sqrtPriceX96 to a conventional price format
|
|
// // Note: The price is calculated as (sqrtPriceX96^2 / 2^192), simplified here as (price / 2^96) for the sake of example
|
|
// uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96);
|
|
|
|
// // Calculate the equivalent token amount for the ETH in the position at the current price
|
|
// // Assuming price is expressed as the amount of token per ETH
|
|
// uint256 equivalentTokenAmountForEth = ethAmountInPosition * price;
|
|
|
|
// // Compare to the actual token amount in the position
|
|
// hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth;
|
|
|
|
// return hasMoreToken;
|
|
// }
|
|
|
|
////////
|
|
// - check if tick in range, otherwise revert
|
|
// - check if the position has more Token or more ETH, at current price
|
|
// - if more ETH,
|
|
// - calculate the amount of Token needed to be minted to bring the position to 50/50
|
|
// - mint
|
|
// - deposit Token into pool
|
|
// - if more TOKEN
|
|
// - calculate the amount of token needed to be withdrawn from the position, to bring the position to 50/50
|
|
// - withdraw
|
|
// - burn tokens
|
|
|
|
|
|
|
|
function stretch(int24 tickLower, int24 tickUpper, uint256 deadline, uint256 amount0Min, uint256 amount1Min) external checkDeadline(deadline) {
|
|
|
|
// Fetch the current tick from the Uniswap V3 pool
|
|
(, int24 currentTick, , , , , ) = pool.slot0();
|
|
|
|
// Check if current tick is within the specified range
|
|
int24 centerTick = tickLower + ((tickUpper - tickLower) / 2);
|
|
// TODO: add hysteresis
|
|
if (token0isWeth) {
|
|
require(currentTick > centerTick && currentTick <= tickUpper, "Current tick out of range for stretch");
|
|
} else {
|
|
require(currentTick >= tickLower && currentTick < centerTick, "Current tick out of range for stretch");
|
|
}
|
|
|
|
(uint256 ethReceived, uint256 oldliquidity) = liquidatePosition(tickLower, tickUpper, amount0Min, amount1Min);
|
|
|
|
uint256 liquidity;
|
|
int24 newTickLower;
|
|
int24 newTickUpper;
|
|
if (token0isWeth) {
|
|
newTickLower = tickLower;
|
|
newTickUpper = currentTick + (currentTick - tickLower);
|
|
// extend the range up
|
|
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
|
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(newTickUpper);
|
|
liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
|
sqrtRatioAX96, sqrtRatioBX96, ethReceived
|
|
);
|
|
// calculate amount for new liquidity
|
|
uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity(
|
|
sqrtRatioAX96, sqrtRatioBX96, liquidity
|
|
);
|
|
uint256 currentBal = harb.balanceOf(address(this));
|
|
if (currentBal < newAmount1) {
|
|
harb.mint(address(this), newAmount1 - currentBal);
|
|
}
|
|
|
|
} else {
|
|
newTickUpper = tickUpper;
|
|
// extend the range down
|
|
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
|
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick));
|
|
liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
|
sqrtRatioAX96, sqrtRatioBX96, ethReceived
|
|
);
|
|
// calculate amount for new liquidity
|
|
uint256 newAmount0 = LiquidityAmounts.getAmount0ForLiquidity(
|
|
sqrtRatioAX96, sqrtRatioBX96, liquidity
|
|
);
|
|
uint256 currentBal = harb.balanceOf(address(this));
|
|
if (currentBal < newAmount0) {
|
|
harb.mint(address(this), newAmount0 - currentBal);
|
|
}
|
|
newTickLower = ...
|
|
}
|
|
addLiquidity(newTickLower, newTickUpper, liquidity, deadline);
|
|
}
|
|
|
|
}
|
|
``` |