diff --git a/.gitmodules b/.gitmodules index 8928653..e0285e5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,4 +13,4 @@ url = https://github.com/eth-infinitism/account-abstraction [submodule "lib/kernel"] path = lib/kernel - url = https://github.com/zerodevapp/kernel + url = https://github.com/zerodevapp/kernel \ No newline at end of file diff --git a/docs/abi/CyberVaultV3.json b/docs/abi/CyberVaultV3.json new file mode 100644 index 0000000..136e587 --- /dev/null +++ b/docs/abi/CyberVaultV3.json @@ -0,0 +1 @@ +[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"_tokenInWhitelist","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"_tokenOut","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"_uniswap","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IUniversalRouter"}],"stateMutability":"view"},{"type":"function","name":"_wrappedNativeCurrency","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"balance","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"balances","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"balancesByCurrency","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"depositPreApprove","inputs":[{"name":"depositTo","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWithPermit","inputs":[{"name":"depositTo","type":"address","internalType":"address"},{"name":"_permit","type":"tuple","internalType":"struct ERC20PermitData","components":[{"name":"currency","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"erc20balances","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"migrateBalance","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"migrateERC20Balance","inputs":[{"name":"currency","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proxiableUUID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"receipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setV3Variables","inputs":[{"name":"uniswap","type":"address","internalType":"address"},{"name":"wrappedNativeCurrency","type":"address","internalType":"address"},{"name":"tokenOut","type":"address","internalType":"address"},{"name":"tokenInList","type":"address[]","internalType":"address[]"},{"name":"tokenInApproved","type":"bool[]","internalType":"bool[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"swapERC20PreApproveAndDeposit","inputs":[{"name":"depositTo","type":"address","internalType":"address"},{"name":"_intent","type":"tuple","internalType":"struct SwapIntent","components":[{"name":"tokenIn","type":"address","internalType":"address"},{"name":"tokenOut","type":"address","internalType":"address"},{"name":"tokenInAmount","type":"uint256","internalType":"uint256"},{"name":"tokenOutAmount","type":"uint256","internalType":"uint256"},{"name":"recipient","type":"address","internalType":"address payable"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"poolFeesTier","type":"uint24","internalType":"uint24"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"swapERC20WithPermitAndDeposit","inputs":[{"name":"depositTo","type":"address","internalType":"address"},{"name":"_intent","type":"tuple","internalType":"struct SwapIntent","components":[{"name":"tokenIn","type":"address","internalType":"address"},{"name":"tokenOut","type":"address","internalType":"address"},{"name":"tokenInAmount","type":"uint256","internalType":"uint256"},{"name":"tokenOutAmount","type":"uint256","internalType":"uint256"},{"name":"recipient","type":"address","internalType":"address payable"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"poolFeesTier","type":"uint24","internalType":"uint24"}]},{"name":"_permit","type":"tuple","internalType":"struct ERC20PermitData","components":[{"name":"currency","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"swapNativeAndDeposit","inputs":[{"name":"depositTo","type":"address","internalType":"address"},{"name":"_intent","type":"tuple","internalType":"struct SwapIntent","components":[{"name":"tokenIn","type":"address","internalType":"address"},{"name":"tokenOut","type":"address","internalType":"address"},{"name":"tokenInAmount","type":"uint256","internalType":"uint256"},{"name":"tokenOutAmount","type":"uint256","internalType":"uint256"},{"name":"recipient","type":"address","internalType":"address payable"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"poolFeesTier","type":"uint24","internalType":"uint24"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"upgradeTo","inputs":[{"name":"newImplementation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"upgradeToAndCall","inputs":[{"name":"newImplementation","type":"address","internalType":"address"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"withdraw","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"withdrawERC20","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"currency","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AdminChanged","inputs":[{"name":"previousAdmin","type":"address","indexed":false,"internalType":"address"},{"name":"newAdmin","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"BeaconUpgraded","inputs":[{"name":"beacon","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Deposit","inputs":[{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositERC20","inputs":[{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"currency","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint8","indexed":false,"internalType":"uint8"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Upgraded","inputs":[{"name":"implementation","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Withdraw","inputs":[{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawERC20","inputs":[{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"currency","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"InvalidPermitSignature","inputs":[]},{"type":"error","name":"SwapFailedBytes","inputs":[{"name":"reason","type":"bytes","internalType":"bytes"}]},{"type":"error","name":"SwapFailedString","inputs":[{"name":"reason","type":"string","internalType":"string"}]}] \ No newline at end of file diff --git a/docs/deploy/op-10/contract.md b/docs/deploy/op-10/contract.md index fe22f7b..8194d70 100644 --- a/docs/deploy/op-10/contract.md +++ b/docs/deploy/op-10/contract.md @@ -23,3 +23,4 @@ | CyberVaultV2(Impl) | 0x041300287e6760196d798be6ce9bd3b485028950 | | Timelock(V2) | 0x640dc26699c95a085086650a18028ab3f1454c81 | | LaunchTokenPool | 0x454ba74c599340b1d868c693ccdb1a55feb8965d | +| CyberVaultV3(Impl) | 0xbf63825b8706ab8999940bf82d660cd9815a89f4 | diff --git a/docs/deploy/op_sepolia-11155420/contract.md b/docs/deploy/op_sepolia-11155420/contract.md index fff5bd0..feee413 100644 --- a/docs/deploy/op_sepolia-11155420/contract.md +++ b/docs/deploy/op_sepolia-11155420/contract.md @@ -21,3 +21,4 @@ | CyberPaymaster | 0x672cf56a66b6f6a0a97f188abe57249fb7eef909 | | LaunchTokenPool | 0xd30e1c72742803de428799c34729168fe70534b2 | | CyberVaultV2(Impl) | 0x251f21e67bd5dcfcf7278fcc5540cd406a2ccc8f | +| CyberVaultV3(Impl) | 0xbf63825b8706ab8999940bf82d660cd9815a89f4 | diff --git a/lib/uniswap/contracts/interfaces/IUniversalRouter.sol b/lib/uniswap/contracts/interfaces/IUniversalRouter.sol new file mode 100644 index 0000000..41a8a80 --- /dev/null +++ b/lib/uniswap/contracts/interfaces/IUniversalRouter.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.14; + +import {IERC721Receiver} from 'openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol'; +import {IERC1155Receiver} from 'openzeppelin-contracts/contracts/token/ERC1155/IERC1155Receiver.sol'; + +interface IUniversalRouter is IERC721Receiver, IERC1155Receiver { + /// @notice Thrown when a required command has failed + error ExecutionFailed(uint256 commandIndex, bytes message); + + /// @notice Thrown when attempting to send ETH directly to the contract + error ETHNotAccepted(); + + /// @notice Thrown when executing commands with an expired deadline + error TransactionDeadlinePassed(); + + /// @notice Thrown when attempting to execute commands and an incorrect number of inputs are provided + error LengthMismatch(); + + /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired. + /// @param commands A set of concatenated commands, each 1 byte in length + /// @param inputs An array of byte strings containing abi encoded inputs for each command + /// @param deadline The deadline by which the transaction must be executed + function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable; +} diff --git a/lib/uniswap/contracts/libraries/Commands.sol b/lib/uniswap/contracts/libraries/Commands.sol new file mode 100644 index 0000000..56fecc5 --- /dev/null +++ b/lib/uniswap/contracts/libraries/Commands.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.14; + +/// @title Commands +/// @notice Command Flags used to decode commands +library Commands { + // Masks to extract certain bits of commands + bytes1 internal constant FLAG_ALLOW_REVERT = 0x80; + bytes1 internal constant COMMAND_TYPE_MASK = 0x3f; + + // Command Types. Maximum supported command at this moment is 0x3f. + + // Command Types where value<0x08, executed in the first nested-if block + uint256 constant V3_SWAP_EXACT_IN = 0x00; + uint256 constant V3_SWAP_EXACT_OUT = 0x01; + uint256 constant PERMIT2_TRANSFER_FROM = 0x02; + uint256 constant PERMIT2_PERMIT_BATCH = 0x03; + uint256 constant SWEEP = 0x04; + uint256 constant TRANSFER = 0x05; + uint256 constant PAY_PORTION = 0x06; + // COMMAND_PLACEHOLDER = 0x07; + + // The commands are executed in nested if blocks to minimise gas consumption + // The following constant defines one of the boundaries where the if blocks split commands + uint256 constant FIRST_IF_BOUNDARY = 0x08; + + // Command Types where 0x08<=value<=0x0f, executed in the second nested-if block + uint256 constant V2_SWAP_EXACT_IN = 0x08; + uint256 constant V2_SWAP_EXACT_OUT = 0x09; + uint256 constant PERMIT2_PERMIT = 0x0a; + uint256 constant WRAP_ETH = 0x0b; + uint256 constant UNWRAP_WETH = 0x0c; + uint256 constant PERMIT2_TRANSFER_FROM_BATCH = 0x0d; + uint256 constant BALANCE_CHECK_ERC20 = 0x0e; + // COMMAND_PLACEHOLDER = 0x0f; + + // The commands are executed in nested if blocks to minimise gas consumption + // The following constant defines one of the boundaries where the if blocks split commands + uint256 constant SECOND_IF_BOUNDARY = 0x10; + + // Command Types where 0x10<=value<0x18, executed in the third nested-if block + uint256 constant SEAPORT_V1_5 = 0x10; + uint256 constant LOOKS_RARE_V2 = 0x11; + uint256 constant NFTX = 0x12; + uint256 constant CRYPTOPUNKS = 0x13; + // 0x14; + uint256 constant OWNER_CHECK_721 = 0x15; + uint256 constant OWNER_CHECK_1155 = 0x16; + uint256 constant SWEEP_ERC721 = 0x17; + + // The commands are executed in nested if blocks to minimise gas consumption + // The following constant defines one of the boundaries where the if blocks split commands + uint256 constant THIRD_IF_BOUNDARY = 0x18; + + // Command Types where 0x18<=value<=0x1f, executed in the final nested-if block + uint256 constant X2Y2_721 = 0x18; + uint256 constant SUDOSWAP = 0x19; + uint256 constant NFT20 = 0x1a; + uint256 constant X2Y2_1155 = 0x1b; + uint256 constant FOUNDATION = 0x1c; + uint256 constant SWEEP_ERC1155 = 0x1d; + uint256 constant ELEMENT_MARKET = 0x1e; + // COMMAND_PLACEHOLDER = 0x1f; + + // The commands are executed in nested if blocks to minimise gas consumption + // The following constant defines one of the boundaries where the if blocks split commands + uint256 constant FOURTH_IF_BOUNDARY = 0x20; + + // Command Types where 0x20<=value + uint256 constant SEAPORT_V1_4 = 0x20; + uint256 constant EXECUTE_SUB_PLAN = 0x21; + uint256 constant APPROVE_ERC20 = 0x22; + // COMMAND_PLACEHOLDER for 0x23 to 0x3f (all unused) +} diff --git a/lib/uniswap/contracts/libraries/Constants.sol b/lib/uniswap/contracts/libraries/Constants.sol new file mode 100644 index 0000000..1f2d7e3 --- /dev/null +++ b/lib/uniswap/contracts/libraries/Constants.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.14; + +/// @title Constant state +/// @notice Constant state used by the Universal Router +library Constants { + /// @dev Used for identifying cases when this contract's balance of a token is to be used as an input + /// This value is equivalent to 1<<255, i.e. a singular 1 in the most significant bit. + uint256 internal constant CONTRACT_BALANCE = 0x8000000000000000000000000000000000000000000000000000000000000000; + + /// @dev Used for identifying cases when a v2 pair has already received input tokens + uint256 internal constant ALREADY_PAID = 0; + + /// @dev Used as a flag for identifying the transfer of ETH instead of a token + address internal constant ETH = address(0); + + /// @dev Used as a flag for identifying that msg.sender should be used, saves gas by sending more 0 bytes + address internal constant MSG_SENDER = address(1); + + /// @dev Used as a flag for identifying address(this) should be used, saves gas by sending more 0 bytes + address internal constant ADDRESS_THIS = address(2); + + /// @dev The length of the bytes encoded address + uint256 internal constant ADDR_SIZE = 20; + + /// @dev The length of the bytes encoded fee + uint256 internal constant V3_FEE_SIZE = 3; + + /// @dev The offset of a single token address (20) and pool fee (3) + uint256 internal constant NEXT_V3_POOL_OFFSET = ADDR_SIZE + V3_FEE_SIZE; + + /// @dev The offset of an encoded pool key + /// Token (20) + Fee (3) + Token (20) = 43 + uint256 internal constant V3_POP_OFFSET = NEXT_V3_POOL_OFFSET + ADDR_SIZE; + + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 internal constant MULTIPLE_V3_POOLS_MIN_LENGTH = V3_POP_OFFSET + NEXT_V3_POOL_OFFSET; +} diff --git a/misc/gen_abi.ts b/misc/gen_abi.ts index 3e04b9d..114151f 100644 --- a/misc/gen_abi.ts +++ b/misc/gen_abi.ts @@ -15,6 +15,7 @@ const writeAbi = async () => { "TokenReceiver.sol/TokenReceiver.json", "CyberVault.sol/CyberVault.json", "CyberVaultV2.sol/CyberVaultV2.json", + "CyberVaultV3.sol/CyberVaultV3.json", "ECDSAValidator.sol/ECDSAValidator.json", "LaunchTokenPool.sol/LaunchTokenPool.json", ]; diff --git a/remappings.txt b/remappings.txt index 14847bf..1827580 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,5 @@ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ solmate/=lib/solmate/ kernel/=lib/kernel/ -account-abstraction/=lib/account-abstraction/contracts/ \ No newline at end of file +account-abstraction/=lib/account-abstraction/contracts/ +universal-router/=lib/uniswap/ \ No newline at end of file diff --git a/script/libraries/LibDeploy.sol b/script/libraries/LibDeploy.sol index b7cb23f..3ff0977 100644 --- a/script/libraries/LibDeploy.sol +++ b/script/libraries/LibDeploy.sol @@ -37,6 +37,7 @@ import { SpecialReward } from "../../src/periphery/SpecialReward.sol"; import { CyberVault } from "../../src/periphery/CyberVault.sol"; import { LaunchTokenPool } from "../../src/periphery/LaunchTokenPool.sol"; import { CyberVaultV2 } from "../../src/periphery/CyberVaultV2.sol"; +import { CyberVaultV3 } from "../../src/periphery/CyberVaultV3.sol"; import { CyberPaymaster } from "../../src/paymaster/CyberPaymaster.sol"; import { UUPSUpgradeable } from "openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol"; @@ -358,13 +359,28 @@ library LibDeploy { function upgradeVault(Vm vm, address _dc, address vaultProxy) internal { Create2Deployer dc = Create2Deployer(_dc); - address cyberVaultV2Impl = dc.deploy( - type(CyberVaultV2).creationCode, + address cyberVaultV3Impl = dc.deploy( + type(CyberVaultV3).creationCode, SALT ); - _write(vm, "CyberVaultV2(Impl)", cyberVaultV2Impl); + _write(vm, "CyberVaultV3(Impl)", cyberVaultV3Impl); - UUPSUpgradeable(vaultProxy).upgradeTo(cyberVaultV2Impl); + UUPSUpgradeable(vaultProxy).upgradeTo(cyberVaultV3Impl); + + address[] memory wl = new address[](2); + wl[0] = address(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85); + wl[1] = address(0x4200000000000000000000000000000000000006); + bool[] memory wlStatus = new bool[](2); + wlStatus[0] = true; + wlStatus[1] = true; + + CyberVaultV3(vaultProxy).setV3Variables( + address(0xCb1355ff08Ab38bBCE60111F1bb2B784bE25D7e8), + address(0x4200000000000000000000000000000000000006), + address(0x94b008aA00579c1307B0EF2c499aD98a8ce58e58), + wl, + wlStatus + ); } function deployVault( diff --git a/src/interfaces/ICyberVault.sol b/src/interfaces/ICyberVault.sol new file mode 100644 index 0000000..1978403 --- /dev/null +++ b/src/interfaces/ICyberVault.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.8.14; + +// Uniswap error selectors, used to surface information when swaps fail +// Pulled from @uniswap/universal-router/out/V3SwapRouter.sol/V3SwapRouter.json after compiling with forge +bytes32 constant V3_INVALID_SWAP = keccak256(hex"316cf0eb"); +bytes32 constant V3_TOO_LITTLE_RECEIVED = keccak256(hex"39d35496"); +bytes32 constant V3_TOO_MUCH_REQUESTED = keccak256(hex"739dbe52"); +bytes32 constant V3_INVALID_AMOUNT_OUT = keccak256(hex"d4e0248e"); +bytes32 constant V3_INVALID_CALLER = keccak256(hex"32b13d91"); + +struct SwapIntent { + address tokenIn; + address tokenOut; + uint256 tokenInAmount; + uint256 tokenOutAmount; + address payable recipient; + uint256 deadline; + uint24 poolFeesTier; +} + +struct ERC20PermitData { + address currency; + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} + +/* + * @title ICyberVault + * @author CyberConnect + */ +interface ICyberVault { + /*////////////////////////////////////////////////////////////// + EVENT + //////////////////////////////////////////////////////////////*/ + event Deposit(address to, uint256 amount); + event Withdraw(address to, uint256 amount); + event DepositERC20(address to, address currency, uint256 amount); + event WithdrawERC20(address to, address currency, uint256 amount); + + /*////////////////////////////////////////////////////////////// + ERROR + //////////////////////////////////////////////////////////////*/ + // @notice Raised when the permit signature is invalid + error InvalidPermitSignature(); + + // @notice Raised when a swap fails and returns a reason string + // @param reason The error reason returned from the swap + error SwapFailedString(string reason); + + // @notice Raised when a swap fails and returns another error + // @param reason The error reason returned from the swap + error SwapFailedBytes(bytes reason); + + /*////////////////////////////////////////////////////////////// + FUNCTION + //////////////////////////////////////////////////////////////*/ + + // deposit recipient currency token with pre-approve tx, will check the allowance before transfer + // @param depositTo the address that deposit to + // @param amount the token amount + function depositPreApprove(address depositTo, uint256 amount) external; + + // deposit recipient currency token with permit signature, will permit token before transfer in this transaction + // @param depositTo the address that deposit to + // @param _permit the permit signature data + function depositWithPermit( + address depositTo, + ERC20PermitData calldata _permit + ) external; + + // swap native currency to recipient currency using UniswapV3, then deposit to recipient + // @param depositTo the address that deposit to + // @param _intent the swap intent + function swapNativeAndDeposit( + address depositTo, + SwapIntent calldata _intent + ) external payable; + + // swap ERC20 currency to recipient currency using UniswapV3 with pre-approve tx, will check the allowance before swap + // @param depositTo the address that deposit to + // @param _intent the swap intent + function swapERC20PreApproveAndDeposit( + address depositTo, + SwapIntent calldata _intent + ) external; + + // swap ERC20 currency to recipient currency using UniswapV3 with permit signature, will permit token before swap in this transaction + // @param depositTo the address that deposit to + // @param _intent the swap intent + function swapERC20WithPermitAndDeposit( + address depositTo, + SwapIntent calldata _intent, + ERC20PermitData calldata _permit + ) external; +} diff --git a/src/periphery/CyberVaultV3.sol b/src/periphery/CyberVaultV3.sol new file mode 100644 index 0000000..f33c8a7 --- /dev/null +++ b/src/periphery/CyberVaultV3.sol @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity 0.8.14; + +import "../interfaces/ICyberVault.sol"; + +import { AccessControl } from "openzeppelin-contracts/contracts/access/AccessControl.sol"; +import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20Permit } from "openzeppelin-contracts/contracts/token/ERC20/extensions/draft-IERC20Permit.sol"; +import { Initializable } from "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol"; +import { ReentrancyGuard } from "openzeppelin-contracts/contracts/security/ReentrancyGuard.sol"; +import "universal-router/contracts/interfaces/IUniversalRouter.sol"; +import { Commands as UniswapCommands } from "universal-router/contracts/libraries/Commands.sol"; +import { Constants as UniswapConstants } from "universal-router/contracts/libraries/Constants.sol"; + +/** + * @title CyberVaultV3 + * @author CyberConnect + * @notice This contract is used to create deposit and distribute tokens. + */ +contract CyberVaultV3 is + Initializable, + AccessControl, + UUPSUpgradeable, + ReentrancyGuard, + ICyberVault +{ + using SafeERC20 for IERC20; + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /*////////////////////////////////////////////////////////////// + V1 STORAGE + //////////////////////////////////////////////////////////////*/ + address public receipient; + mapping(address => uint256) public balances; + mapping(address => mapping(address => uint256)) public balancesByCurrency; + + bytes32 internal constant _OPERATOR_ROLE = + keccak256(bytes("OPERATOR_ROLE")); + + /*////////////////////////////////////////////////////////////// + V2 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public balance; + mapping(address => uint256) public erc20balances; + + /*////////////////////////////////////////////////////////////// + V3 STORAGE + //////////////////////////////////////////////////////////////*/ + + // Represents native token of a chain (e.g. ETH or MATIC) + address private immutable _NATIVE_CURRENCY = address(0); + // Canonical wrapped token for this chain. e.g. (wETH or wMATIC). + address public _wrappedNativeCurrency; + // Uniswap on-chain contract + IUniversalRouter public _uniswap; + // The swap tokenIn whitelist + mapping(address => bool) public _tokenInWhitelist; + // The currency that the recipient wants to receive (e.g. USDT) + address public _tokenOut; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor() { + _disableInitializers(); + } + + /*////////////////////////////////////////////////////////////// + EXTERNAL + //////////////////////////////////////////////////////////////*/ + + function depositPreApprove( + address depositTo, + uint256 amount + ) external nonReentrant { + IERC20 token = IERC20(_tokenOut); + require(token.balanceOf(msg.sender) >= amount, "INSUFFICIENT_BALANCE"); + require( + token.allowance(msg.sender, address(this)) >= amount, + "INSUFFICIENT_ALLOWANCE" + ); + + // Perform and complete the deposit + token.safeTransferFrom(msg.sender, address(this), amount); + _succeedDeposit(depositTo, amount); + } + + function depositWithPermit( + address depositTo, + ERC20PermitData calldata _permit + ) external nonReentrant { + require(_permit.currency == _tokenOut, "INVALID_CURRENCY"); + require( + IERC20(_tokenOut).balanceOf(msg.sender) >= _permit.amount, + "INSUFFICIENT_BALANCE" + ); + + // Permit the token transfer + try + IERC20Permit(_tokenOut).permit( + msg.sender, + address(this), + _permit.amount, + _permit.deadline, + _permit.v, + _permit.r, + _permit.s + ) + {} catch { + revert InvalidPermitSignature(); + } + + // Perform and complete the deposit + IERC20(_tokenOut).safeTransferFrom( + msg.sender, + address(this), + _permit.amount + ); + _succeedDeposit(depositTo, _permit.amount); + } + + function swapNativeAndDeposit( + address depositTo, + SwapIntent calldata _intent + ) external payable nonReentrant validSwapIntent(_intent) { + require(_intent.tokenInAmount == msg.value, "AMOUNT_MISMATCH"); + require(_intent.tokenIn == _wrappedNativeCurrency, "INVALID_TOKEN_IN"); + + // Perform the swap + uint256 amountSwapped = _swapTokens(_intent); + + // Complete the deposit + _succeedDeposit(depositTo, amountSwapped); + } + + function swapERC20PreApproveAndDeposit( + address depositTo, + SwapIntent calldata _intent + ) external nonReentrant validSwapIntent(_intent) { + IERC20 tokenIn = IERC20(_intent.tokenIn); + require( + tokenIn.balanceOf(msg.sender) >= _intent.tokenInAmount, + "INSUFFICIENT_BALANCE" + ); + require( + tokenIn.allowance(msg.sender, address(this)) >= + _intent.tokenInAmount, + "INSUFFICIENT_ALLOWANCE" + ); + + // Transfer the payment token to this contract + tokenIn.safeTransferFrom( + msg.sender, + address(this), + _intent.tokenInAmount + ); + + // Perform the swap + uint256 amountSwapped = _swapTokens(_intent); + + // Complete the deposit + _succeedDeposit(depositTo, amountSwapped); + } + + function swapERC20WithPermitAndDeposit( + address depositTo, + SwapIntent calldata _intent, + ERC20PermitData calldata _permit + ) external nonReentrant validSwapIntent(_intent) { + require(_intent.tokenInAmount == _permit.amount, "AMOUNT_MISMATCH"); + require(_intent.tokenIn == _permit.currency, "INVALID_TOKEN_IN"); + require( + IERC20(_intent.tokenIn).balanceOf(msg.sender) >= + _intent.tokenInAmount, + "INSUFFICIENT_BALANCE" + ); + + uint256 amountSwapped = 0; + // Permit the token transfer + try + IERC20Permit(_intent.tokenIn).permit( + msg.sender, + address(this), + _permit.amount, + _permit.deadline, + _permit.v, + _permit.r, + _permit.s + ) + {} catch { + revert InvalidPermitSignature(); + } + // Transfer the payment token to this contract + IERC20(_intent.tokenIn).safeTransferFrom( + msg.sender, + address(this), + _permit.amount + ); + // Perform the swap + amountSwapped = _swapTokens(_intent); + // Complete the deposit + _succeedDeposit(depositTo, amountSwapped); + } + + /*////////////////////////////////////////////////////////////// + ONLY OPERATOR + //////////////////////////////////////////////////////////////*/ + + function withdraw( + address to, + uint256 amount + ) external onlyRole(_OPERATOR_ROLE) { + (bool success, ) = to.call{ value: amount }(""); + require(success, "WITHDRAW_FAILED"); + emit Withdraw(to, amount); + } + + function withdrawERC20( + address to, + address currency, + uint256 amount + ) external onlyRole(_OPERATOR_ROLE) { + IERC20(currency).safeTransfer(to, amount); + emit WithdrawERC20(to, currency, amount); + } + + function migrateBalance(uint256 amount) external onlyRole(_OPERATOR_ROLE) { + balance = amount; + } + + function migrateERC20Balance( + address currency, + uint256 amount + ) external onlyRole(_OPERATOR_ROLE) { + erc20balances[currency] = amount; + } + + /*////////////////////////////////////////////////////////////// + ONLY OWNER + //////////////////////////////////////////////////////////////*/ + + function _authorizeUpgrade(address) internal view override { + require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "ONLY_ADMIN"); + } + + function setV3Variables( + address uniswap, + address wrappedNativeCurrency, + address tokenOut, + address[] calldata tokenInList, + bool[] calldata tokenInApproved + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require( + tokenInList.length == tokenInApproved.length, + "INVALID_ARRAY_LENGTH" + ); + _tokenOut = tokenOut; + _uniswap = IUniversalRouter(uniswap); + _wrappedNativeCurrency = wrappedNativeCurrency; + for (uint i = 0; i < tokenInList.length; i++) { + _tokenInWhitelist[tokenInList[uint(i)]] = tokenInApproved[uint(i)]; + } + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function _succeedDeposit(address depositTo, uint256 amount) internal { + if (amount > 0) { + erc20balances[_tokenOut] += amount; + emit DepositERC20(depositTo, _tokenOut, amount); + } + } + + function _swapTokens( + SwapIntent calldata _intent + ) internal returns (uint256) { + // Parameters and shared inputs for the universal router + bytes memory uniswapCommands; + bytes[] memory uniswapInputs; + bytes memory swapPath = abi.encodePacked( + _intent.tokenOut, + _intent.poolFeesTier, + _intent.tokenIn + ); + bytes memory swapParams = abi.encode( + address(_uniswap), + _intent.tokenOutAmount, + _intent.tokenInAmount, + swapPath, + false + ); + uint256 deadline = _intent.deadline; + bytes memory transferToRecipient = abi.encode( + _intent.tokenOut, + _intent.recipient, + _intent.tokenOutAmount + ); + + // The payer's and router's balances before this transaction, used to calculate the amount consumed by the swap + uint256 payerBalanceBefore; + uint256 routerBalanceBefore; + uint256 recipientBalanceBefore; + + // Populate the commands and inputs for the universal router + if (msg.value > 0) { + // Paying with ETH + payerBalanceBefore = msg.sender.balance + msg.value; + routerBalanceBefore = + address(_uniswap).balance + + IERC20(_wrappedNativeCurrency).balanceOf(address(_uniswap)); + recipientBalanceBefore = IERC20(_intent.tokenOut).balanceOf( + _intent.recipient + ); + + // Paying with ETH, wrapping it to WETH, then swapping it for the output token + uniswapCommands = abi.encodePacked( + bytes1(uint8(UniswapCommands.WRAP_ETH)), // wrap ETH to WETH + bytes1(uint8(UniswapCommands.V3_SWAP_EXACT_OUT)), // swap WETH for tokenOut + bytes1(uint8(UniswapCommands.TRANSFER)), // transfer tokenOut to recipient + bytes1(uint8(UniswapCommands.UNWRAP_WETH)), // unwrap WETH to ETH for the payer refund (if any left) + bytes1(uint8(UniswapCommands.SWEEP)) // sweep any remaining ETH to the payer + ); + uniswapInputs = new bytes[](5); + uniswapInputs[0] = abi.encode(address(_uniswap), msg.value); + uniswapInputs[1] = swapParams; + uniswapInputs[2] = transferToRecipient; + uniswapInputs[3] = abi.encode(address(_uniswap), 0); + uniswapInputs[4] = abi.encode(UniswapConstants.ETH, msg.sender, 0); + } else { + // Paying with tokenIn (ERC20) + payerBalanceBefore = + IERC20(_intent.tokenIn).balanceOf(msg.sender) + + _intent.tokenInAmount; + routerBalanceBefore = IERC20(_intent.tokenIn).balanceOf( + address(_uniswap) + ); + recipientBalanceBefore = IERC20(_intent.tokenOut).balanceOf( + _intent.recipient + ); + + // Paying with tokenIn, recipient wants tokenOut + uniswapCommands = abi.encodePacked( + bytes1(uint8(UniswapCommands.V3_SWAP_EXACT_OUT)), // swap tokenIn for tokenOut + bytes1(uint8(UniswapCommands.TRANSFER)), // transfer tokenOut to recipient + bytes1(uint8(UniswapCommands.SWEEP)) // sweep any remaining tokenIn to the payer + ); + uniswapInputs = new bytes[](3); + uniswapInputs[0] = swapParams; + uniswapInputs[1] = transferToRecipient; + uniswapInputs[2] = abi.encode(_intent.tokenIn, msg.sender, 0); + + // Send the input tokens to Uniswap for the swap + IERC20(_intent.tokenIn).safeTransfer( + address(_uniswap), + _intent.tokenInAmount + ); + } + + // Perform the swap + try + _uniswap.execute{ value: msg.value }( + uniswapCommands, + uniswapInputs, + deadline + ) + { + // Calculate and return how much of the input token was consumed by the swap. The router + // could have had a balance of the input token prior to this transaction, which would have + // been swept to the payer. This amount, if any, must be accounted for so we don't underflow + // and assume that negative amount of the input token was consumed by the swap. + uint256 payerBalanceAfter; + uint256 routerBalanceAfter; + if (msg.value > 0) { + payerBalanceAfter = msg.sender.balance; + routerBalanceAfter = + address(_uniswap).balance + + IERC20(_wrappedNativeCurrency).balanceOf(address(_uniswap)); + } else { + payerBalanceAfter = IERC20(_intent.tokenIn).balanceOf( + msg.sender + ); + routerBalanceAfter = IERC20(_intent.tokenIn).balanceOf( + address(_uniswap) + ); + } + return + (payerBalanceBefore + routerBalanceBefore) - + (payerBalanceAfter + routerBalanceAfter); + } catch Error(string memory reason) { + revert SwapFailedString(reason); + } catch (bytes memory reason) { + bytes32 reasonHash = keccak256(reason); + if (reasonHash == V3_INVALID_SWAP) { + revert SwapFailedString("V3InvalidSwap"); + } else if (reasonHash == V3_TOO_LITTLE_RECEIVED) { + revert SwapFailedString("V3TooLittleReceived"); + } else if (reasonHash == V3_TOO_MUCH_REQUESTED) { + revert SwapFailedString("V3TooMuchRequested"); + } else if (reasonHash == V3_INVALID_AMOUNT_OUT) { + revert SwapFailedString("V3InvalidAmountOut"); + } else if (reasonHash == V3_INVALID_CALLER) { + revert SwapFailedString("V3InvalidCaller"); + } else { + revert SwapFailedBytes(reason); + } + } + } + + // @dev Raises errors if the SwapIntent is invalid + modifier validSwapIntent(SwapIntent calldata _intent) { + require(_intent.deadline >= block.timestamp, "EXPIRED_INTENT"); + + require(_intent.recipient == address(this), "INVALID_RECIPIENT"); + + require(_tokenInWhitelist[_intent.tokenIn], "INVALID_TOKEN_IN"); + + require(_intent.tokenOut == _tokenOut, "INVALID_TOKEN_OUT"); + + require( + _intent.tokenInAmount != 0 && _intent.tokenOutAmount != 0, + "INVALID_AMOUNT" + ); + + _; + } +}