diff --git a/README.md b/README.md index 0f051aa..de623b9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## p2p-yield-proxy Contracts for depositing and withdrawing ERC-20 tokens from yield protocols. -The current implementation is only compatible with [Ethena](https://ethena.fi/) protocol. +The current implementation is only compatible with [Superform](https://www.superform.xyz/) protocol. ## Running tests @@ -18,25 +18,35 @@ forge test forge script script/Deploy.s.sol:Deploy --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --chain $CHAIN_ID --json --verify --etherscan-api-key $ETHERSCAN_API_KEY -vvvvv ``` +`Deploy.s.sol` deterministically deploys the shared Superform tooling using CREATE2 so that every network produces the same contract addresses. +Set the following environment variables (override the defaults only when a network requires different endpoints): + +- `PRIVATE_KEY` – broadcaster key (also becomes the default `P2P_SIGNER`) +- `P2P_SIGNER` – optional override for the signer account +- `P2P_OPERATOR` – optional override for the operator account (defaults to `P2P_SIGNER`) +- `SUPERFORM_ROUTER` +- `SUPER_POSITIONS` +- `P2P_TREASURY` +- `REWARDS_DISTRIBUTOR` + This script will: -- deploy and verify on Etherscan the **P2pEthenaProxyFactory** and **P2pEthenaProxy** contracts -- set the **P2pTreasury** address permanently in the P2pEthenaProxyFactory -- set the rules for Ethena specific deposit and withdrawal functions +- deploy and verify on Etherscan the **P2pSuperformProxyFactory**, its reference **P2pSuperformProxy**, the **AllowedCalldataChecker** implementation, proxy, and **ProxyAdmin** +- ensure all CREATE2 salts are reused so the deployed addresses stay identical on Base, Optimism, Mainnet, and any additional chains ## Basic use case ![Basic use case diagram](image-1.png) -#### Ethena Deposit flow +#### Superform Deposit flow -Look at [function _doDeposit()](test/MainnetIntegration.sol#L1000) for a reference implementation of the flow. +Look at [function _doDeposit()](test/OptimismUSDT.t.sol#L430) for a reference implementation of the flow. 1. Website User (called Client in contracts) calls Backend with its (User's) Ethereum address and some Merchant info. 2. Backend uses Merchant info to determine the P2P fee (expressed as client basis points in the contracts). -3. Backend calls P2pEthenaProxyFactory's `getHashForP2pSigner` function to get the hash for the P2pSigner. +3. Backend calls P2pSuperformProxyFactory's `getHashForP2pSigner` function to get the hash for the P2pSigner. ```solidity /// @dev Gets the hash for the P2pSigner @@ -46,101 +56,96 @@ Look at [function _doDeposit()](test/MainnetIntegration.sol#L1000) for a referen /// @return The hash for the P2pSigner function getHashForP2pSigner( address _client, - uint96 _clientBasisPoints, + uint48 _clientBasisPoints, uint256 _p2pSignerSigDeadline ) external view returns (bytes32); ``` 4. Backend signs the hash with the P2pSigner's private key using `eth_sign`. Signing is necessary to authenticate the client basis points in the contracts. -5. Backend returns JSON to the User with (client address, client basis points, signature deadline, and the signature). +5. Backend calls Superform API to generate Superform deposit calldata. + +6. Backend returns JSON to the User with (client address, client basis points, signature deadline, and the signature). -6. Client-side JS code prepares all the necessary data for the Morpho deposit function. Since the deposited tokens will first go from the client to the client's P2pEthenaProxy instance and then from the P2pEthenaProxy instance into the Ethena protocol, both of these transfers are approved by the client via Permit2. The client's P2pEthenaProxy instance address is fetched from the P2pEthenaProxyFactory contract's `predictP2pEthenaProxyAddress` function: +7. Client-side JS code prepares all the necessary data for the Morpho deposit function. The client's P2pSuperformProxy instance address is fetched from the P2pSuperformProxyFactory contract's `predictP2pYieldProxyAddress` function: ```solidity - /// @dev Computes the address of a P2pEthenaProxy created by `_createP2pEthenaProxy` function - /// @dev P2pEthenaProxy instances are guaranteed to have the same address if _feeDistributorInstance is the same + /// @dev Computes the address of a P2pYieldProxy created by `_getOrCreateP2pYieldProxy` function + /// @dev P2pYieldProxy instances are guaranteed to have the same address if _feeDistributorInstance is the same /// @param _client The address of client - /// @return address The address of the P2pEthenaProxy instance - function predictP2pEthenaProxyAddress( + /// @param _clientBasisPointsOfDeposit The client basis points (share) of deposit + /// @param _clientBasisPointsOfProfit The client basis points (share) of profit + /// @return address The address of the P2pYieldProxy instance + function predictP2pYieldProxyAddress( address _client, - uint96 _clientBasisPoints + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit ) external view returns (address); ``` -7. Client-side JS code checks if User has already approved the required amount of the deposited token for Permit2. If not, it prompts the User to call the `approve` function of the deposited token contract with the uint256 MAX value and Permit2 contract as the spender. +8. Client-side JS code checks if the user has already approved the required amount of the deposited token for the P2pSuperformProxy instance. If not, it prompts the user to call the ERC20 `approve` function with an allowance that covers the intended deposit amount. -8. Client-side JS code prompts the User to do `eth_signTypedData_v4` to sign `PermitSingle` from the User's wallet into the P2pEthenaProxy instance - -9. Client-side JS code prompts the User to call the `deposit` function of the P2pEthenaProxyFactory contract: +9. Client-side JS code prompts the user to call the `deposit` function of the P2pSuperformProxyFactory contract: ```solidity - /// @dev Deposits the yield protocol - /// @param _permitSingleForP2pEthenaProxy The permit single for P2pEthenaProxy - /// @param _permit2SignatureForP2pEthenaProxy The permit2 signature for P2pEthenaProxy - /// @param _clientBasisPoints The client basis points + /// @dev Initiates a deposit through a client specific P2pYieldProxy instance + /// @param _yieldProtocolCalldata Yield protocol calldata + /// @param _clientBasisPointsOfDeposit The client basis points (share) of deposit + /// @param _clientBasisPointsOfProfit The client basis points (share) of profit /// @param _p2pSignerSigDeadline The P2pSigner signature deadline /// @param _p2pSignerSignature The P2pSigner signature - /// @return P2pEthenaProxyAddress The client's P2pEthenaProxy instance address + /// @return p2pYieldProxyAddress The client's P2pYieldProxy instance address function deposit( - IAllowanceTransfer.PermitSingle memory _permitSingleForP2pEthenaProxy, - bytes calldata _permit2SignatureForP2pEthenaProxy, - - uint96 _clientBasisPoints, + bytes calldata _yieldProtocolCalldata, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, uint256 _p2pSignerSigDeadline, bytes calldata _p2pSignerSignature ) external - returns (address P2pEthenaProxyAddress); + payable + returns (address p2pYieldProxyAddress); ``` -#### Ethena Withdrawal flow +#### Superform Withdrawal flow -Look at [function _doWithdraw()](test/MainnetIntegration.sol#L1024) for a reference implementation of the flow. +Look at [function _doWithdraw()](test/OptimismUSDT.t.sol#L486) for a reference implementation of the flow. -1. Client-side JS code prepares all the necessary data for the Ethena `cooldownShares` function. +1. Website User calls P2P.org's backend for Superform withdrawal calldata. -2. Client-side JS code prompts the User to call the `cooldownShares` function of the client's instance of the P2pEthenaProxy contract: +2. P2P.org's backend calls Superform API for Superform withdrawal calldata. -```solidity - /// @notice redeem shares into assets and starts a cooldown to claim the converted underlying asset - /// @param _shares shares to redeem - function cooldownShares(uint256 _shares) external returns (uint256 assets); -``` +3. P2P.org's backend returns Superform withdrawal calldata to the User. -3. Wait for the [cooldownDuration](https://etherscan.io/address/0x9d39a5de30e57443bff2a8307a4256c8797a3497#readContract#F9). (Currently, 7 days). +4. Client-side JS code prepares all the necessary data for the Superform `singleDirectSingleVaultWithdraw` function. -2. Client-side JS code prompts the User to call the `withdrawAfterCooldown` function of the client's instance of the P2pEthenaProxy contract: +5. Client-side JS code prompts the User to call the `withdraw` function of the client's instance of the P2pSuperformProxy contract: ```solidity - /// @notice withdraw assets after cooldown has elapsed - function withdrawAfterCooldown() external; + /// @notice Withdraw assets from Superform protocol + /// @param _superformCalldata calldata for withdraw function of Superform protocol + function withdraw( + bytes calldata _superformCalldata + ) external; ``` -The P2pEthenaProxy contract will redeem the tokens from Ethena and send them to User. The amount on top of the deposited amount is split between the User and the P2pTreasury according to the client basis points. +The P2pSuperformProxy contract will redeem the tokens from Superform and send them to User. The amount on top of the deposited amount is split between the User and the P2pTreasury according to the client basis points. -## Calling any function on any contracts via P2pEthenaProxy +## Calling any function on any contracts via P2pSuperformProxy -It's possible for the User to call any function on any contracts via P2pEthenaProxy. This can be useful if it appears that functions of yield protocols beyond simple deposit and withdrawal are needed. Also, it can be useful for claiming any airdrops unknown in advance. +It's possible for the User to call any function on any contracts via P2pSuperformProxy. This can be useful if it appears that functions of yield protocols beyond simple deposit and withdrawal are needed. Also, it can be useful for claiming any airdrops unknown in advance. -Before the User can use this feature, the P2P operator needs to set the rules for the function call via the `setCalldataRules` function of the P2pEthenaProxyFactory contract: +Before the User can use this feature, the P2P operator needs to deploy a new contract implementing the `IAllowedCalldataChecker` interface and then the function `upgrade` function on ProxyAdmin: ```solidity - /// @dev Sets the calldata rules - /// @param _contract The contract address - /// @param _selector The selector - /// @param _rules The rules - function setCalldataRules( - address _contract, - bytes4 _selector, - P2pStructs.Rule[] calldata _rules - ) external; + MockAllowedCalldataChecker newImplementation = new MockAllowedCalldataChecker(); + admin.upgrade(ITransparentUpgradeableProxy(address(tup)), address(newImplementation)); ``` The rules should be as strict as possible to prevent any undesired function calls. -Once the rules are set, the User can call the permitted function on the permitted contract with the permitted calldata via P2pEthenaProxy's `callAnyFunction` function: +Once the rules are set, the User can call the permitted function on the permitted contract with the permitted calldata via P2pSuperformProxy's `callAnyFunction` function: ```solidity /// @notice Calls an arbitrary allowed function diff --git a/foundry.toml b/foundry.toml index 7f57923..1a7c975 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,4 +1,6 @@ [profile.default] +ffi = true +fs_permissions = [{ access = "read-write", path = "./" }] src = "src" out = "out" libs = ["lib"] @@ -9,4 +11,4 @@ via_ir = true optimizer = true optimizer-runs = 2000 -rpc_endpoints = { mainnet = "https://rpc.ankr.com/eth", sepolia = "https://rpc.ankr.com/eth_sepolia", base = "https://mainnet.base.org" } +rpc_endpoints = { mainnet = "https://eth.drpc.org", optimism = "https://mainnet.optimism.io", base = "https://mainnet.base.org" } diff --git a/image-1.png b/image-1.png index a5f7fb2..8ec71b1 100644 Binary files a/image-1.png and b/image-1.png differ diff --git a/lib/forge-std b/lib/forge-std index 999be66..17a9b23 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 999be66ec94c78090087f674bbf52ce1cca12b75 +Subproject commit 17a9b2398a7d629931dc66a168a098d051d53dc6 diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index dc7d33c..314af64 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -4,32 +4,171 @@ pragma solidity 0.8.27; import "../lib/forge-std/src/Vm.sol"; -import "../src/adapters/ethena/p2pEthenaProxyFactory/P2pEthenaProxyFactory.sol"; +import "../lib/forge-std/src/console2.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "../src/adapters/superform/p2pSuperformProxy/P2pSuperformProxy.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import "../src/common/AllowedCalldataChecker.sol"; import {Script} from "forge-std/Script.sol"; contract Deploy is Script { - address constant USDe = 0x4c9EDD5852cd905f086C759E8383e09bff1E68B3; - address constant sUSDe = 0x9D39A5DE30e57443BfF2A8307A4256c8797A3497; - address constant P2pTreasury = 0xfeef177E6168F9b7fd59e6C5b6c2d87FF398c6FD; + struct DeploymentConfig { + address superformRouter; + address superPositions; + address p2pTreasury; + address rewardsDistributor; + address p2pSigner; + address p2pOperator; + } - function run() - external - returns (P2pEthenaProxyFactory factory, P2pEthenaProxy proxy) - { + struct DeploymentResult { + ProxyAdmin proxyAdmin; + AllowedCalldataChecker allowedCalldataCheckerImplementation; + TransparentUpgradeableProxy allowedCalldataCheckerProxy; + P2pSuperformProxyFactory factory; + P2pSuperformProxy referenceProxy; + } + + bytes32 private constant SALT_PROXY_ADMIN = keccak256("p2p.superform.proxy_admin.v2"); + bytes32 private constant SALT_ALLOWED_CALLDATA_CHECKER_IMPL = + keccak256("p2p.superform.allowed_calldata_checker.impl.v2"); + bytes32 private constant SALT_ALLOWED_CALLDATA_CHECKER_PROXY = + keccak256("p2p.superform.allowed_calldata_checker.proxy.v2"); + bytes32 private constant SALT_FACTORY = keccak256("p2p.superform.proxy_factory.v2"); + + address private constant DEFAULT_SUPERFORM_ROUTER = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address private constant DEFAULT_SUPER_POSITIONS = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address private constant DEFAULT_P2P_TREASURY = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + address private constant DEFAULT_REWARDS_DISTRIBUTOR = 0xce23bD7205bF2B543F6B4eeC00Add0C111FEFc3B; + + function run() external returns (DeploymentResult memory deployment) { + DeploymentConfig memory config = _loadConfig(); uint256 deployerKey = vm.envUint("PRIVATE_KEY"); - Vm.Wallet memory wallet = vm.createWallet(deployerKey); + address deployer = vm.addr(deployerKey); + config.p2pSigner = vm.envOr("P2P_SIGNER", deployer); + config.p2pOperator = vm.envOr("P2P_OPERATOR", config.p2pSigner); + + console2.log("Deploying with signer:", config.p2pSigner); + console2.log("Using operator:", config.p2pOperator); + console2.log("Broadcast sender:", deployer); + console2.log("Target chain ID:", block.chainid); vm.startBroadcast(deployerKey); - factory = new P2pEthenaProxyFactory( - wallet.addr, - P2pTreasury, - sUSDe, - USDe - ); + + ProxyAdmin proxyAdmin = _deployProxyAdmin(deployer); + AllowedCalldataChecker allowedImplementation = _deployAllowedCalldataChecker(deployer); + TransparentUpgradeableProxy allowedProxy = + _deployAllowedCalldataCheckerProxy(deployer, proxyAdmin, allowedImplementation); + P2pSuperformProxyFactory factory = _deployFactory(deployer, config, address(allowedProxy)); + vm.stopBroadcast(); - proxy = P2pEthenaProxy(factory.getReferenceP2pYieldProxy()); + P2pSuperformProxy referenceProxy = P2pSuperformProxy(payable(factory.getReferenceP2pYieldProxy())); + + console2.log("ProxyAdmin:", address(proxyAdmin)); + console2.log("AllowedCalldataChecker implementation:", address(allowedImplementation)); + console2.log("AllowedCalldataChecker proxy:", address(allowedProxy)); + console2.log("P2pSuperformProxyFactory:", address(factory)); + console2.log("Reference P2pSuperformProxy:", address(referenceProxy)); + + deployment = DeploymentResult({ + proxyAdmin: proxyAdmin, + allowedCalldataCheckerImplementation: allowedImplementation, + allowedCalldataCheckerProxy: allowedProxy, + factory: factory, + referenceProxy: referenceProxy + }); + } + + function _loadConfig() private view returns (DeploymentConfig memory config) { + address router = vm.envOr("SUPERFORM_ROUTER", DEFAULT_SUPERFORM_ROUTER); + address superPositions = vm.envOr("SUPER_POSITIONS", DEFAULT_SUPER_POSITIONS); + address treasury = vm.envOr("P2P_TREASURY", DEFAULT_P2P_TREASURY); + address rewards = vm.envOr("REWARDS_DISTRIBUTOR", DEFAULT_REWARDS_DISTRIBUTOR); + + config.superformRouter = router; + config.superPositions = superPositions; + config.p2pTreasury = treasury; + config.rewardsDistributor = rewards; + } + + function _deployProxyAdmin(address deployer) private returns (ProxyAdmin proxyAdmin) { + bytes memory bytecode = type(ProxyAdmin).creationCode; + address predicted = vm.computeCreate2Address(SALT_PROXY_ADMIN, keccak256(bytecode), deployer); + + if (predicted.code.length == 0) { + proxyAdmin = new ProxyAdmin{salt: SALT_PROXY_ADMIN}(); + } else { + proxyAdmin = ProxyAdmin(predicted); + } + } + + function _deployAllowedCalldataChecker(address deployer) + private + returns (AllowedCalldataChecker allowedCalldataChecker) + { + bytes memory bytecode = type(AllowedCalldataChecker).creationCode; + address predicted = vm.computeCreate2Address(SALT_ALLOWED_CALLDATA_CHECKER_IMPL, keccak256(bytecode), deployer); + + if (predicted.code.length == 0) { + allowedCalldataChecker = new AllowedCalldataChecker{salt: SALT_ALLOWED_CALLDATA_CHECKER_IMPL}(); + } else { + allowedCalldataChecker = AllowedCalldataChecker(predicted); + } + } - return (factory, proxy); + function _deployAllowedCalldataCheckerProxy( + address deployer, + ProxyAdmin proxyAdmin, + AllowedCalldataChecker implementation + ) private returns (TransparentUpgradeableProxy proxy) { + bytes memory initData = abi.encodeWithSelector(AllowedCalldataChecker.initialize.selector); + bytes memory bytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(address(implementation), address(proxyAdmin), initData) + ); + address predicted = vm.computeCreate2Address(SALT_ALLOWED_CALLDATA_CHECKER_PROXY, keccak256(bytecode), deployer); + + if (predicted.code.length == 0) { + proxy = new TransparentUpgradeableProxy{salt: SALT_ALLOWED_CALLDATA_CHECKER_PROXY}( + address(implementation), address(proxyAdmin), initData + ); + } else { + proxy = TransparentUpgradeableProxy(payable(predicted)); + } + } + + function _deployFactory(address deployer, DeploymentConfig memory config, address allowedCalldataCheckerProxy) + private + returns (P2pSuperformProxyFactory factory) + { + bytes memory bytecode = abi.encodePacked( + type(P2pSuperformProxyFactory).creationCode, + abi.encode( + config.p2pSigner, + config.p2pOperator, + config.p2pTreasury, + config.superformRouter, + config.superPositions, + allowedCalldataCheckerProxy, + config.rewardsDistributor + ) + ); + address predicted = vm.computeCreate2Address(SALT_FACTORY, keccak256(bytecode), deployer); + + if (predicted.code.length == 0) { + factory = new P2pSuperformProxyFactory{salt: SALT_FACTORY}( + config.p2pSigner, + config.p2pOperator, + config.p2pTreasury, + config.superformRouter, + config.superPositions, + allowedCalldataCheckerProxy, + config.rewardsDistributor + ); + } else { + factory = P2pSuperformProxyFactory(predicted); + } } } diff --git a/script/DeployBase.s.sol b/script/DeployBase.s.sol new file mode 100644 index 0000000..25c1973 --- /dev/null +++ b/script/DeployBase.s.sol @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import "../src/common/AllowedCalldataChecker.sol"; +import {Script} from "forge-std/Script.sol"; + +contract DeployBase is Script { + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + address constant RewardsDistributor = 0xce23bD7205bF2B543F6B4eeC00Add0C111FEFc3B; + + function run() external returns (P2pSuperformProxyFactory factory, P2pSuperformProxy proxy) { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + vm.startBroadcast(deployerKey); + + AllowedCalldataChecker implementation = new AllowedCalldataChecker(); + ProxyAdmin admin = new ProxyAdmin(); + bytes memory initData = abi.encodeWithSelector(AllowedCalldataChecker.initialize.selector); + TransparentUpgradeableProxy tup = + new TransparentUpgradeableProxy(address(implementation), address(admin), initData); + factory = new P2pSuperformProxyFactory( + wallet.addr, wallet.addr, P2pTreasury, SuperformRouter, SuperPositions, address(tup), RewardsDistributor + ); + + vm.stopBroadcast(); + + proxy = P2pSuperformProxy(payable(factory.getReferenceP2pYieldProxy())); + + return (factory, proxy); + } +} diff --git a/script/DeployOptimism.s.sol b/script/DeployOptimism.s.sol new file mode 100644 index 0000000..b174c5a --- /dev/null +++ b/script/DeployOptimism.s.sol @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import "../src/common/AllowedCalldataChecker.sol"; +import {Script} from "forge-std/Script.sol"; + +contract DeployOptimism is Script { + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + address constant RewardsDistributor = 0xce23bD7205bF2B543F6B4eeC00Add0C111FEFc3B; + + function run() external returns (P2pSuperformProxyFactory factory, P2pSuperformProxy proxy) { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + vm.startBroadcast(deployerKey); + + AllowedCalldataChecker implementation = new AllowedCalldataChecker(); + ProxyAdmin admin = new ProxyAdmin(); + bytes memory initData = abi.encodeWithSelector(AllowedCalldataChecker.initialize.selector); + TransparentUpgradeableProxy tup = + new TransparentUpgradeableProxy(address(implementation), address(admin), initData); + factory = new P2pSuperformProxyFactory( + wallet.addr, wallet.addr, P2pTreasury, SuperformRouter, SuperPositions, address(tup), RewardsDistributor + ); + + vm.stopBroadcast(); + + proxy = P2pSuperformProxy(payable(factory.getReferenceP2pYieldProxy())); + + return (factory, proxy); + } +} diff --git a/script/RunTestDepositBase.s.sol b/script/RunTestDepositBase.s.sol new file mode 100644 index 0000000..e78e9b4 --- /dev/null +++ b/script/RunTestDepositBase.s.sol @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import {Script} from "forge-std/Script.sol"; + +contract RunTestDepositBase is Script { + using SafeERC20 for IERC20; + + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + + address constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + uint256 constant SigDeadline = 1743997707; + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10000; // 0% fee + uint256 constant DepositAmount = 123400; + uint256 constant SharesAmount = 1222092; + uint256 constant SuperformId = 53060340969225753329461353767745054384708953976330005872281754; + + P2pSuperformProxyFactory factory; + address proxyAddress; + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + factory = P2pSuperformProxyFactory(0x27aC94F64Fa26d8E26e3833c6E0ce7e24227656D); + proxyAddress = + factory.predictP2pYieldProxyAddress(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + bytes memory p2pSignerSignature = + _getP2pSignerSignature(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + LiqRequest memory liqRequest = LiqRequest({ + txData: "", + token: USDC, + interimToken: address(0), + bridgeId: 1, + liqDstChainId: 0, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: DepositAmount, + outputAmount: SharesAmount, + maxSlippage: 50, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = + hex"b19dcc3300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000210500000001668bcc80d9b85de4e683a5e1d64946e175a3a748000000000000000000000000000000000000000000000000000000000001e208000000000000000000000000000000000000000000000000000000000001dcb600000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e1158d9158d41186994b400ab833b85284f2e06c000000000000000000000000e1158d9158d41186994b400ab833b85284f2e06c000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; //abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.startBroadcast(deployerKey); + // Approve the proxy to spend USDC tokens + if (IERC20(USDC).allowance(wallet.addr, proxyAddress) == 0) { + IERC20(USDC).safeApprove(proxyAddress, type(uint256).max); + } + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopBroadcast(); + } + + function _getP2pSignerSignature( + address _clientAddress, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, + uint256 _sigDeadline + ) private view returns (bytes memory) { + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + _clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _sigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(vm.envUint("PRIVATE_KEY"), ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + return p2pSignerSignature; + } +} diff --git a/script/RunTestDepositNativeOptimism.s.sol b/script/RunTestDepositNativeOptimism.s.sol new file mode 100644 index 0000000..c9998d1 --- /dev/null +++ b/script/RunTestDepositNativeOptimism.s.sol @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import {Script} from "forge-std/Script.sol"; + +contract RunTestDepositNativeOptimism is Script { + using SafeERC20 for IERC20; + + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + + address constant USDT = 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; + uint256 constant SigDeadline = 1746420983; + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10000; // 0% fee + uint256 constant DepositAmount = 12340000000000000; + + P2pSuperformProxyFactory factory; + address proxyAddress; + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + factory = P2pSuperformProxyFactory(0xEF3c3034FD8E396b11713183C7080811E6cFD026); + proxyAddress = + factory.predictP2pYieldProxyAddress(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + bytes memory p2pSignerSignature = + _getP2pSignerSignature(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + bytes memory superformCalldata = + hex"b19dcc3300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000a00000001b8138fff124dd7f91abf412b73be453fb140568c00000000000000000000000000000000000000000000000000248238191b12ce00000000000000000000000000000000000000000000000000247a8437f408dc00000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b3055ebe6b9c43c86fb684d512123fd88f8a6f19000000000000000000000000b3055ebe6b9c43c86fb684d512123fd88f8a6f190000000000000000000000000000000000000000000000000000000000000c2000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000002bd72a2487400000000000000000000000000000000000000000000000000000000000000009a44630a0d8f8cf2c5ebadc417dcbfbeb1f04438e7bbf259fee0d2d9c72f870d72eba1fc7a300000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000b8138fff124dd7f91abf412b73be453fb140568c0000000000000000000000000000000000000000000000000022aee87e401eaa0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30783030303030303030303030303030303030303030303030303030303030303030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b50000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb000000000000000000000000000000000000000000000000002bd72a2487400000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000006e4e21fd0e90000000000000000000000000000000000000000000000000000000000000020000000000000000000000000c7d3ab410d49b664d03fe5b1038852ac852b1b29000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000164010300000048000000ba12222222228d8ba445958a75a0704d566bf2c839965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003020000000000000000002bd72a248740000b00000039000000ba12222222228d8ba445958a75a0704d566bf2c89da11ff60bfc5af527f58fd61679c3ac98d040d900000000000000000000010000010b0000002e02000000ee1bac98527a9fdd57fccf967817215b083ce1f001010000000000000000000000000000000100eed0b40aeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee1f32b1c2345538c0c6f582fcb022739c4a194ebb1231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000006809faee00000054000000000000000000000000000000000000000000000000000000000000000000000000000000000000000264838984000000000000000000248238191b12ce4f82e73edb06d29ff62c91ec8f5ff06571bdeb2900000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000002bd72a248740000000000000000000000000000000000000000000000000000022aee87e401eaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025d7b22536f75726365223a226c692e6669222c22416d6f756e74496e555344223a2232312e353635373136363038383835303538222c22416d6f756e744f7574555344223a2232312e3533303838303233373730393835222c22526566657272616c223a22222c22466c616767733a302c22416d6f756e744f7574223a223130323736323736363132353637373538222c2254696d657374616d70223a313734353438333332362c22526f7574654944223a2263326437633637662d356338622d343961312d613232652d616164613631383065386234222c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a224d34424131515533726935564b424148616e443250687435567130654654314c6e7056354c542f50474a364259514379714e7838744855412b6a486633414a64454c74625378635658544e6e56763349317a3431746c68774f524c576475596664426e394a55446b4373624f5a58474e543644335554694e636c47485032356170415035465950706164624c6c34377653616c59676d78795565585a3946704d49686944446442737431344e53472f4c596a6e345a5a6e697276697770776835534b6c74396d7074674538436c432f456b35594351636c424b64337765697767316b546c436e36573543553547786b643742634b544a76504e4a6c70496a307962674a523742384848472b574f535865686c4d3865635a4d762b7242675570504c756e4b4552644f7942492f48584b4343506c4654324144775171367957394e67725249386e5557524a37544e4554513150334e33413d3d227d7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; //abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.startBroadcast(deployerKey); + factory.deposit{value: (12340000000000000) * 113 / 100}( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopBroadcast(); + } + + function _getP2pSignerSignature( + address _clientAddress, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, + uint256 _sigDeadline + ) private view returns (bytes memory) { + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + _clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _sigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(vm.envUint("PRIVATE_KEY"), ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + return p2pSignerSignature; + } +} diff --git a/script/RunTestDepositOptimism.s.sol b/script/RunTestDepositOptimism.s.sol new file mode 100644 index 0000000..62657bc --- /dev/null +++ b/script/RunTestDepositOptimism.s.sol @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import {Script} from "forge-std/Script.sol"; + +contract RunTestDepositOptimism is Script { + using SafeERC20 for IERC20; + + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + + address constant USDT = 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; + uint256 constant SigDeadline = 1743997707; + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10000; // 0% fee + uint256 constant DepositAmount = 123400; + + P2pSuperformProxyFactory factory; + address proxyAddress; + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + factory = P2pSuperformProxyFactory(0x99F3cfC4dBd64AB26D8aa6f9148F815CBc81E3Cc); + proxyAddress = + factory.predictP2pYieldProxyAddress(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + bytes memory p2pSignerSignature = + _getP2pSignerSignature(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + bytes memory superformCalldata = + hex"b19dcc3300000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000a0000000197116661c85c4e1ee35aa10f7fc5fe5e67b83a5b00000000000000000000000000000000000000000000000001b8ca1b687f4d6000000000000000000000000000000000000000000000000001b8ca1b687f4d6000000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000006a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f22028570f6f6a5d717ad99f726050411e11eb20000000000000000000000001f22028570f6f6a5d717ad99f726050411e11eb200000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004444630a0d896ba9cffae8a22aa75ffdc6910e52d52ee9a199ee31eb8893dc693d7c89ed4a800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000097116661c85c4e1ee35aa10f7fc5fe5e67b83a5b00000000000000000000000000000000000000000000000001a2c000701289810000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30783030303030303030303030303030303030303030303030303030303030303030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc0000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc00000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b2819000000000000000000000000000000000000000000000000000000000001e20800000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001842646478b00000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000000000000000000000000000000000000001e208000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b281900000000000000000000000000000000000000000000000001a2c000701289810000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000840294b008aa00579c1307b0ef2c499ad98a8ce58e5801ffff01962e23cd3f58f887a5238082a75d223f71890629006140b987d6b51fd75b66c3b07733beb5167c42fc010b2c639c533813f4aa9d7837caf62653d097ff8501ffff018ac2f9dac7a2852d44f3c09634444d533e4c078e011231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; //abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.startBroadcast(deployerKey); + // Approve the proxy to spend USDT tokens + if (IERC20(USDT).allowance(wallet.addr, proxyAddress) == 0) { + IERC20(USDT).safeApprove(proxyAddress, type(uint256).max); + } + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopBroadcast(); + } + + function _getP2pSignerSignature( + address _clientAddress, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, + uint256 _sigDeadline + ) private view returns (bytes memory) { + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + _clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _sigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(vm.envUint("PRIVATE_KEY"), ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + return p2pSignerSignature; + } +} diff --git a/script/RunTestWithdrawBase.s.sol b/script/RunTestWithdrawBase.s.sol new file mode 100644 index 0000000..4e05506 --- /dev/null +++ b/script/RunTestWithdrawBase.s.sol @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import {Script} from "forge-std/Script.sol"; + +contract RunTestWithdrawBase is Script { + using SafeERC20 for IERC20; + + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10000; // 0% fee + + P2pSuperformProxyFactory factory; + address proxyAddress; + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + factory = P2pSuperformProxyFactory(0x2b2CBe3Cb583EDDa67B6121E29962405C9856FE9); + proxyAddress = + factory.predictP2pYieldProxyAddress(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + bytes memory superformCalldata = + hex"407c7b1d00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000210500000001668bcc80d9b85de4e683a5e1d64946e175a3a748000000000000000000000000000000000000000000000000000000000001dcb5000000000000000000000000000000000000000000000000000000000001e20700000000000000000000000000000000000000000000000000000000000013880000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e1158d9158d41186994b400ab833b85284f2e06c000000000000000000000000e1158d9158d41186994b400ab833b85284f2e06c000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; //abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.startBroadcast(deployerKey); + P2pSuperformProxy(payable(proxyAddress)).withdraw(superformCalldata); + vm.stopBroadcast(); + } +} diff --git a/script/RunTestWithdrawNativeOptimism.s.sol b/script/RunTestWithdrawNativeOptimism.s.sol new file mode 100644 index 0000000..3f3c4b0 --- /dev/null +++ b/script/RunTestWithdrawNativeOptimism.s.sol @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import {Script} from "forge-std/Script.sol"; + +contract RunTestWithdrawNativeOptimism is Script { + using SafeERC20 for IERC20; + + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10000; // 0% fee + + P2pSuperformProxyFactory factory; + address proxyAddress; + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + factory = P2pSuperformProxyFactory(0xEF3c3034FD8E396b11713183C7080811E6cFD026); + proxyAddress = + factory.predictP2pYieldProxyAddress(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + bytes memory superformCalldata = + hex"407c7b1d00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000a00000001b8138fff124dd7f91abf412b73be453fb140568c00000000000000000000000000000000000000000000000000246bce0022c1630000000000000000000000000000000000000000000000000024737ec621e13800000000000000000000000000000000000000000000000000000000000013880000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000076000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b3055ebe6b9c43c86fb684d512123fd88f8a6f19000000000000000000000000b3055ebe6b9c43c86fb684d512123fd88f8a6f19000000000000000000000000000000000000000000000000000000000000078000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005044630a0d83e20e4c98c0e7de0d36b81a9b48d0c4a88617f17c0d65659ae71752f4c62daba00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000b3055ebe6b9c43c86fb684d512123fd88f8a6f190000000000000000000000000000000000000000000000000015d983e600aeef0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a3078303030303030303030303030303030303030303030303030303030303030303030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000060e8c01e8e39b10202e39e62001f08092cc03ca000000000000000000000000060e8c01e8e39b10202e39e62001f08092cc03ca0000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024737ec621e13800000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002442646478b0000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb0000000000000000000000000000000000000000000000000024737ec621e138000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000015d983e600aeef0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014c021f32b1c2345538c0c6f582fcb022739c4a194ebb0395550104f6c85a1b00f6d9b75f91fd23835974cc07e65c01060e8c01e8e39b10202e39e62001f08092cc03ca66660195d7d146ae40d4822c2750276b54b6eed530d37401060e8c01e8e39b10202e39e62001f08092cc03caffff01766854992bd5363ebeeff0113f5a5795796befab01060e8c01e8e39b10202e39e62001f08092cc03ca017f5c764cbc14f9669b88837ca1490cca17c3160701ffff0185149247691df622eaf1a8bd0cafd40bc45154a900060e8c01e8e39b10202e39e62001f08092cc03ca0194b008aa00579c1307b0ef2c499ad98a8ce58e5801ffff01c858a329bf053be78d6239c4a4343b8fbd21472b00060e8c01e8e39b10202e39e62001f08092cc03ca01420000000000000000000000000000000000000601ffff02001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + vm.startBroadcast(deployerKey); + P2pSuperformProxy(payable(proxyAddress)).withdraw(superformCalldata); + vm.stopBroadcast(); + } +} diff --git a/script/RunTestWithdrawOptimism.s.sol b/script/RunTestWithdrawOptimism.s.sol new file mode 100644 index 0000000..8cbca10 --- /dev/null +++ b/script/RunTestWithdrawOptimism.s.sol @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../lib/forge-std/src/Vm.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import {Script} from "forge-std/Script.sol"; + +contract RunTestWithdrawOptimism is Script { + using SafeERC20 for IERC20; + + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10000; // 0% fee + + P2pSuperformProxyFactory factory; + address proxyAddress; + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + Vm.Wallet memory wallet = vm.createWallet(deployerKey); + + factory = P2pSuperformProxyFactory(0x99F3cfC4dBd64AB26D8aa6f9148F815CBc81E3Cc); + proxyAddress = + factory.predictP2pYieldProxyAddress(wallet.addr, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + bytes memory superformCalldata = + hex"407c7b1d00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000a0000000197116661c85c4e1ee35aa10f7fc5fe5e67b83a5b000000000000000000000000000000000000000000000000002c14939f0666fb000000000000000000000000000000000000000000000000002c14939f0666fb000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f22028570f6f6a5d717ad99f726050411e11eb20000000000000000000000001f22028570f6f6a5d717ad99f726050411e11eb2000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e5800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004044630a0d8db58392a5ec14b23ef56401c814f1ce0bff3fd4f7f74c06e092670b4c587c7a600000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000001f22028570f6f6a5d717ad99f726050411e11eb2000000000000000000000000000000000000000000000000000000000000181a0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30783030303030303030303030303030303030303030303030303030303030303030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc0000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b281900000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000000000000000000000000000002c14939f0666fb00000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001442646478b000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b2819000000000000000000000000000000000000000000000000002c14939f0666fb00000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000000000000000000000000000000000000000181a0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004202c40f949f8a4e094d1b49a23ea9241d289b7b281901ffff01e8a05463f7a2796e1bf11a25d317f17ed7fce5e7001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; //abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.startBroadcast(deployerKey); + P2pSuperformProxy(payable(proxyAddress)).withdraw(superformCalldata); + vm.stopBroadcast(); + } +} diff --git a/src/@openzeppelin/contracts-upgradable/proxy/utils/Initializable.sol b/src/@openzeppelin/contracts-upgradable/proxy/utils/Initializable.sol new file mode 100644 index 0000000..121b2e8 --- /dev/null +++ b/src/@openzeppelin/contracts-upgradable/proxy/utils/Initializable.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol) + +pragma solidity 0.8.27; + +import "../../utils/AddressUpgradeable.sol"; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ```solidity + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. + * + * Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a + * constructor. + * + * Emits an {Initialized} event. + */ + modifier initializer() { + bool isTopLevelCall = !_initializing; + require( + (isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1), + "Initializable: contract is already initialized" + ); + _initialized = 1; + if (isTopLevelCall) { + _initializing = true; + } + _; + if (isTopLevelCall) { + _initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * A reinitializer may be used after the original initialization step. This is essential to configure modules that + * are added through upgrades and that require initialization. + * + * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` + * cannot be nested. If one is invoked in the context of another, execution will revert. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + * + * WARNING: setting the version to 255 will prevent any future reinitialization. + * + * Emits an {Initialized} event. + */ + modifier reinitializer(uint8 version) { + require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + _initialized = version; + _initializing = true; + _; + _initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + require(_initializing, "Initializable: contract is not initializing"); + _; + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + * + * Emits an {Initialized} event the first time it is successfully executed. + */ + function _disableInitializers() internal virtual { + require(!_initializing, "Initializable: contract is initializing"); + if (_initialized != type(uint8).max) { + _initialized = type(uint8).max; + emit Initialized(type(uint8).max); + } + } + + /** + * @dev Returns the highest version that has been initialized. See {reinitializer}. + */ + function _getInitializedVersion() internal view returns (uint8) { + return _initialized; + } + + /** + * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. + */ + function _isInitializing() internal view returns (bool) { + return _initializing; + } +} diff --git a/src/@openzeppelin/contracts/security/ReentrancyGuard.sol b/src/@openzeppelin/contracts-upgradable/security/ReentrancyGuardUpgradeable.sol similarity index 81% rename from src/@openzeppelin/contracts/security/ReentrancyGuard.sol rename to src/@openzeppelin/contracts-upgradable/security/ReentrancyGuardUpgradeable.sol index 60a0fa2..86365e7 100644 --- a/src/@openzeppelin/contracts/security/ReentrancyGuard.sol +++ b/src/@openzeppelin/contracts-upgradable/security/ReentrancyGuardUpgradeable.sol @@ -2,6 +2,9 @@ // OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol) pragma solidity 0.8.27; +import {Initializable} from "../proxy/utils/Initializable.sol"; + +import {Initializable} from "../proxy/utils/Initializable.sol"; /** * @dev Contract module that helps prevent reentrant calls to a function. @@ -19,7 +22,7 @@ pragma solidity 0.8.27; * to protect against it, check out our blog post * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. */ -abstract contract ReentrancyGuard { +abstract contract ReentrancyGuardUpgradeable is Initializable { // Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write @@ -36,7 +39,11 @@ abstract contract ReentrancyGuard { uint256 private _status; - constructor() { + function __ReentrancyGuard_init() internal onlyInitializing { + __ReentrancyGuard_init_unchained(); + } + + function __ReentrancyGuard_init_unchained() internal onlyInitializing { _status = _NOT_ENTERED; } @@ -74,4 +81,11 @@ abstract contract ReentrancyGuard { function _reentrancyGuardEntered() internal view returns (bool) { return _status == _ENTERED; } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; } diff --git a/src/@openzeppelin/contracts-upgradable/utils/AddressUpgradeable.sol b/src/@openzeppelin/contracts-upgradable/utils/AddressUpgradeable.sol new file mode 100644 index 0000000..1850880 --- /dev/null +++ b/src/@openzeppelin/contracts-upgradable/utils/AddressUpgradeable.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) + +pragma solidity 0.8.27; + +/** + * @dev Collection of functions related to the address type + */ +library AddressUpgradeable { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * + * Furthermore, `isContract` will also return true if the target contract within + * the same transaction is already scheduled for destruction by `SELFDESTRUCT`, + * which only has an effect at the end of a transaction. + * ==== + * + * [IMPORTANT] + * ==== + * You shouldn't rely on `isContract` to protect against flash loan attacks! + * + * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets + * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract + * constructor. + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize/address.code.length, which returns 0 + // for contracts in construction, since the code is only stored at the end + // of the constructor execution. + + return account.code.length > 0; + } + + /** + * @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.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + (bool success,) = recipient.call{value: amount}(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @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, it is bubbled up by this + * function (like regular Solidity function calls). + * + * 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. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) + internal + returns (bytes memory) + { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @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`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) + internal + returns (bytes memory) + { + require(address(this).balance >= value, "Address: insufficient balance for call"); + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data, string memory errorMessage) + internal + view + returns (bytes memory) + { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data, string memory errorMessage) + internal + returns (bytes memory) + { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata, errorMessage); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling + * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * + * _Available since v4.8._ + */ + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata, + string memory errorMessage + ) internal view returns (bytes memory) { + if (success) { + if (returndata.length == 0) { + // only check isContract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + require(isContract(target), "Address: call to non-contract"); + } + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + /** + * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason or using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) + internal + pure + returns (bytes memory) + { + if (success) { + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + function _revert(bytes memory returndata, string memory errorMessage) 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(errorMessage); + } + } +} diff --git a/src/@openzeppelin/contracts/access/Ownable.sol b/src/@openzeppelin/contracts/access/Ownable.sol new file mode 100644 index 0000000..c181ea1 --- /dev/null +++ b/src/@openzeppelin/contracts/access/Ownable.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) + +pragma solidity ^0.8.0; + +import "../utils/Context.sol"; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _transferOwnership(_msgSender()); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby disabling any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} diff --git a/src/@openzeppelin/contracts/interfaces/IERC1967.sol b/src/@openzeppelin/contracts/interfaces/IERC1967.sol new file mode 100644 index 0000000..833fcc2 --- /dev/null +++ b/src/@openzeppelin/contracts/interfaces/IERC1967.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1967.sol) + +pragma solidity 0.8.27; + +/** + * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. + * + * _Available since v4.8.3._ + */ +interface IERC1967 { + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Emitted when the beacon is changed. + */ + event BeaconUpgraded(address indexed beacon); +} diff --git a/src/@openzeppelin/contracts/interfaces/IERC4626.sol b/src/@openzeppelin/contracts/interfaces/IERC4626.sol index 88b299e..0deacde 100644 --- a/src/@openzeppelin/contracts/interfaces/IERC4626.sol +++ b/src/@openzeppelin/contracts/interfaces/IERC4626.sol @@ -16,11 +16,7 @@ interface IERC4626 is IERC20, IERC20Metadata { event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); event Withdraw( - address indexed sender, - address indexed receiver, - address indexed owner, - uint256 assets, - uint256 shares + address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares ); /** diff --git a/src/@openzeppelin/contracts/interfaces/draft-IERC1822.sol b/src/@openzeppelin/contracts/interfaces/draft-IERC1822.sol new file mode 100644 index 0000000..523df63 --- /dev/null +++ b/src/@openzeppelin/contracts/interfaces/draft-IERC1822.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) + +pragma solidity 0.8.27; + +/** + * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified + * proxy whose upgrades are fully controlled by the current implementation. + */ +interface IERC1822Proxiable { + /** + * @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation + * address. + * + * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this + * function revert if invoked through a proxy. + */ + function proxiableUUID() external view returns (bytes32); +} diff --git a/src/@openzeppelin/contracts/proxy/Clones.sol b/src/@openzeppelin/contracts/proxy/Clones.sol index 71954e0..156d6c9 100644 --- a/src/@openzeppelin/contracts/proxy/Clones.sol +++ b/src/@openzeppelin/contracts/proxy/Clones.sol @@ -54,11 +54,11 @@ library Clones { /** * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. */ - function predictDeterministicAddress( - address implementation, - bytes32 salt, - address deployer - ) internal pure returns (address predicted) { + function predictDeterministicAddress(address implementation, bytes32 salt, address deployer) + internal + pure + returns (address predicted) + { assembly ("memory-safe") { let ptr := mload(0x40) mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) diff --git a/src/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol b/src/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol new file mode 100644 index 0000000..6f32279 --- /dev/null +++ b/src/@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) + +pragma solidity 0.8.27; + +import "../Proxy.sol"; +import "./ERC1967Upgrade.sol"; + +/** + * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an + * implementation address that can be changed. This address is stored in storage in the location specified by + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the + * implementation behind the proxy. + */ +contract ERC1967Proxy is Proxy, ERC1967Upgrade { + /** + * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. + * + * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded + * function call, and allows initializing the storage of the proxy like a Solidity constructor. + */ + constructor(address _logic, bytes memory _data) payable { + _upgradeToAndCall(_logic, _data, false); + } + + /** + * @dev Returns the current implementation address. + */ + function _implementation() internal view virtual override returns (address impl) { + return ERC1967Upgrade._getImplementation(); + } +} diff --git a/src/@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/src/@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol new file mode 100644 index 0000000..60a41d1 --- /dev/null +++ b/src/@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Upgrade.sol) + +pragma solidity 0.8.27; + +import "../beacon/IBeacon.sol"; +import "../../interfaces/IERC1967.sol"; +import "../../interfaces/draft-IERC1822.sol"; +import "../../utils/Address.sol"; +import "../../utils/StorageSlot.sol"; + +/** + * @dev This abstract contract provides getters and event emitting update functions for + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + * + * _Available since v4.1._ + */ +abstract contract ERC1967Upgrade is IERC1967 { + // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 + bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Returns the current implementation address. + */ + function _getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Perform implementation upgrade + * + * Emits an {Upgraded} event. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Perform implementation upgrade with additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { + _upgradeTo(newImplementation); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(newImplementation, data); + } + } + + /** + * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { + // Upgrades from old implementations will perform a rollback test. This test requires the new + // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing + // this special case will break upgrade paths from old UUPS implementation to new ones. + if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { + _setImplementation(newImplementation); + } else { + try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { + require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); + } catch { + revert("ERC1967Upgrade: new implementation is not UUPS"); + } + _upgradeToAndCall(newImplementation, data, forceCall); + } + } + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Returns the current admin. + */ + function _getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + require(newAdmin != address(0), "ERC1967: new admin is the zero address"); + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + */ + function _changeAdmin(address newAdmin) internal { + emit AdminChanged(_getAdmin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. + * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. + */ + bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /** + * @dev Returns the current beacon. + */ + function _getBeacon() internal view returns (address) { + return StorageSlot.getAddressSlot(_BEACON_SLOT).value; + } + + /** + * @dev Stores a new beacon in the EIP1967 beacon slot. + */ + function _setBeacon(address newBeacon) private { + require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract"); + require( + Address.isContract(IBeacon(newBeacon).implementation()), "ERC1967: beacon implementation is not a contract" + ); + StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; + } + + /** + * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does + * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). + * + * Emits a {BeaconUpgraded} event. + */ + function _upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { + _setBeacon(newBeacon); + emit BeaconUpgraded(newBeacon); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); + } + } +} diff --git a/src/@openzeppelin/contracts/proxy/Proxy.sol b/src/@openzeppelin/contracts/proxy/Proxy.sol new file mode 100644 index 0000000..57c6e7c --- /dev/null +++ b/src/@openzeppelin/contracts/proxy/Proxy.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) + +pragma solidity 0.8.27; + +/** + * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM + * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to + * be specified by overriding the virtual {_implementation} function. + * + * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a + * different contract through the {_delegate} function. + * + * The success and return data of the delegated call will be returned back to the caller of the proxy. + */ +abstract contract Proxy { + /** + * @dev Delegates the current call to `implementation`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _delegate(address implementation) internal virtual { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /** + * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function + * and {_fallback} should delegate. + */ + function _implementation() internal view virtual returns (address); + + /** + * @dev Delegates the current call to the address returned by `_implementation()`. + * + * This function does not return to its internal call site, it will return directly to the external caller. + */ + function _fallback() internal virtual { + _beforeFallback(); + _delegate(_implementation()); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other + * function in the contract matches the call data. + */ + fallback() external payable virtual { + _fallback(); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data + * is empty. + */ + receive() external payable virtual { + _fallback(); + } + + /** + * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` + * call, or as part of the Solidity `fallback` or `receive` functions. + * + * If overridden should call `super._beforeFallback()`. + */ + function _beforeFallback() internal virtual {} +} diff --git a/src/@openzeppelin/contracts/proxy/beacon/IBeacon.sol b/src/@openzeppelin/contracts/proxy/beacon/IBeacon.sol new file mode 100644 index 0000000..64beccf --- /dev/null +++ b/src/@openzeppelin/contracts/proxy/beacon/IBeacon.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) + +pragma solidity 0.8.27; + +/** + * @dev This is the interface that {BeaconProxy} expects of its beacon. + */ +interface IBeacon { + /** + * @dev Must return an address that can be used as a delegate call target. + * + * {BeaconProxy} will check that this address is a contract. + */ + function implementation() external view returns (address); +} diff --git a/src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol b/src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol new file mode 100644 index 0000000..cf8fbe0 --- /dev/null +++ b/src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/ProxyAdmin.sol) + +pragma solidity 0.8.27; + +import "./TransparentUpgradeableProxy.sol"; +import "../../access/Ownable.sol"; + +/** + * @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an + * explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}. + */ +contract ProxyAdmin is Ownable { + /** + * @dev Returns the current implementation of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function getProxyImplementation(ITransparentUpgradeableProxy proxy) public view virtual returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("implementation()")) == 0x5c60da1b + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Returns the current admin of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function getProxyAdmin(ITransparentUpgradeableProxy proxy) public view virtual returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("admin()")) == 0xf851a440 + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Changes the admin of `proxy` to `newAdmin`. + * + * Requirements: + * + * - This contract must be the current admin of `proxy`. + */ + function changeProxyAdmin(ITransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { + proxy.changeAdmin(newAdmin); + } + + /** + * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function upgrade(ITransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { + proxy.upgradeTo(implementation); + } + + /** + * @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See + * {TransparentUpgradeableProxy-upgradeToAndCall}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function upgradeAndCall(ITransparentUpgradeableProxy proxy, address implementation, bytes memory data) + public + payable + virtual + onlyOwner + { + proxy.upgradeToAndCall{value: msg.value}(implementation, data); + } +} diff --git a/src/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/src/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol new file mode 100644 index 0000000..dadf1d5 --- /dev/null +++ b/src/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/transparent/TransparentUpgradeableProxy.sol) + +pragma solidity 0.8.27; + +import "../ERC1967/ERC1967Proxy.sol"; + +/** + * @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy} + * does not implement this interface directly, and some of its functions are implemented by an internal dispatch + * mechanism. The compiler is unaware that these functions are implemented by {TransparentUpgradeableProxy} and will not + * include them in the ABI so this interface must be used to interact with it. + */ +interface ITransparentUpgradeableProxy is IERC1967 { + function admin() external view returns (address); + + function implementation() external view returns (address); + + function changeAdmin(address) external; + + function upgradeTo(address) external; + + function upgradeToAndCall(address, bytes memory) external payable; +} + +/** + * @dev This contract implements a proxy that is upgradeable by an admin. + * + * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector + * clashing], which can potentially be used in an attack, this contract uses the + * https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two + * things that go hand in hand: + * + * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if + * that call matches one of the admin functions exposed by the proxy itself. + * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the + * implementation. If the admin tries to call a function on the implementation it will fail with an error that says + * "admin cannot fallback to proxy target". + * + * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing + * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due + * to sudden errors when trying to call a function from the proxy implementation. + * + * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, + * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + * + * NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not + * inherit from that interface, and instead the admin functions are implicitly implemented using a custom dispatch + * mechanism in `_fallback`. Consequently, the compiler will not produce an ABI for this contract. This is necessary to + * fully implement transparency without decoding reverts caused by selector clashes between the proxy and the + * implementation. + * + * WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the compiler + * will not check that there are no selector conflicts, due to the note above. A selector clash between any new function + * and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could + * render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised. + */ +contract TransparentUpgradeableProxy is ERC1967Proxy { + /** + * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and + * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. + */ + constructor(address _logic, address admin_, bytes memory _data) payable ERC1967Proxy(_logic, _data) { + _changeAdmin(admin_); + } + + /** + * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. + * + * CAUTION: This modifier is deprecated, as it could cause issues if the modified function has arguments, and the + * implementation provides a function with the same selector. + */ + modifier ifAdmin() { + if (msg.sender == _getAdmin()) { + _; + } else { + _fallback(); + } + } + + /** + * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior + */ + function _fallback() internal virtual override { + if (msg.sender == _getAdmin()) { + bytes memory ret; + bytes4 selector = msg.sig; + if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) { + ret = _dispatchUpgradeTo(); + } else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) { + ret = _dispatchUpgradeToAndCall(); + } else if (selector == ITransparentUpgradeableProxy.changeAdmin.selector) { + ret = _dispatchChangeAdmin(); + } else if (selector == ITransparentUpgradeableProxy.admin.selector) { + ret = _dispatchAdmin(); + } else if (selector == ITransparentUpgradeableProxy.implementation.selector) { + ret = _dispatchImplementation(); + } else { + revert("TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + } + assembly { + return(add(ret, 0x20), mload(ret)) + } + } else { + super._fallback(); + } + } + + /** + * @dev Returns the current admin. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function _dispatchAdmin() private returns (bytes memory) { + _requireZeroValue(); + + address admin = _getAdmin(); + return abi.encode(admin); + } + + /** + * @dev Returns the current implementation. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` + */ + function _dispatchImplementation() private returns (bytes memory) { + _requireZeroValue(); + + address implementation = _implementation(); + return abi.encode(implementation); + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + */ + function _dispatchChangeAdmin() private returns (bytes memory) { + _requireZeroValue(); + + address newAdmin = abi.decode(msg.data[4:], (address)); + _changeAdmin(newAdmin); + + return ""; + } + + /** + * @dev Upgrade the implementation of the proxy. + */ + function _dispatchUpgradeTo() private returns (bytes memory) { + _requireZeroValue(); + + address newImplementation = abi.decode(msg.data[4:], (address)); + _upgradeToAndCall(newImplementation, bytes(""), false); + + return ""; + } + + /** + * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified + * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the + * proxied contract. + */ + function _dispatchUpgradeToAndCall() private returns (bytes memory) { + (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); + _upgradeToAndCall(newImplementation, data, true); + + return ""; + } + + /** + * @dev Returns the current admin. + * + * CAUTION: This function is deprecated. Use {ERC1967Upgrade-_getAdmin} instead. + */ + function _admin() internal view virtual returns (address) { + return _getAdmin(); + } + + /** + * @dev To keep this contract fully transparent, all `ifAdmin` functions must be payable. This helper is here to + * emulate some proxy functions being non-payable while still allowing value to pass through. + */ + function _requireZeroValue() private { + require(msg.value == 0); + } +} diff --git a/src/@openzeppelin/contracts/token/ERC1155/IERC1155.sol b/src/@openzeppelin/contracts/token/ERC1155/IERC1155.sol new file mode 100644 index 0000000..a193b33 --- /dev/null +++ b/src/@openzeppelin/contracts/token/ERC1155/IERC1155.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/IERC1155.sol) + +pragma solidity 0.8.27; + +import "../../utils/introspection/IERC165.sol"; + +/** + * @dev Required interface of an ERC1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * + * _Available since v3.1._ + */ +interface IERC1155 is IERC165 { + /** + * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. + */ + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + /** + * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all + * transfers. + */ + event TransferBatch( + address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values + ); + + /** + * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to + * `approved`. + */ + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + + /** + * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI. + * + * If an {URI} event was emitted for `id`, the standard + * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value + * returned by {IERC1155MetadataURI-uri}. + */ + event URI(string value, uint256 indexed id); + + /** + * @dev Returns the amount of tokens of token type `id` owned by `account`. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function balanceOf(address account, uint256 id) external view returns (uint256); + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}. + * + * Requirements: + * + * - `accounts` and `ids` must have the same length. + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) + external + view + returns (uint256[] memory); + + /** + * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`, + * + * Emits an {ApprovalForAll} event. + * + * Requirements: + * + * - `operator` cannot be the caller. + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns true if `operator` is approved to transfer ``account``'s tokens. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address account, address operator) external view returns (bool); + + /** + * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. + * + * Emits a {TransferSingle} event. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}. + * - `from` must have a balance of tokens of type `id` of at least `amount`. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the + * acceptance magic value. + */ + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; + + /** + * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. + * + * Emits a {TransferBatch} event. + * + * Requirements: + * + * - `ids` and `amounts` must have the same length. + * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the + * acceptance magic value. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} diff --git a/src/@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol b/src/@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol new file mode 100644 index 0000000..cea626b --- /dev/null +++ b/src/@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) + +pragma solidity 0.8.27; + +import "../../utils/introspection/IERC165.sol"; + +/** + * @dev _Available since v3.1._ + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev Handles the receipt of a single ERC1155 token type. This function is + * called at the end of a `safeTransferFrom` after the balance has been updated. + * + * NOTE: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param operator The address which initiated the transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param id The ID of the token being transferred + * @param value The amount of tokens being transferred + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + */ + function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) + external + returns (bytes4); + + /** + * @dev Handles the receipt of a multiple ERC1155 token types. This function + * is called at the end of a `safeBatchTransferFrom` after the balances have + * been updated. + * + * NOTE: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param operator The address which initiated the batch transfer (i.e. msg.sender) + * @param from The address which previously owned the token + * @param ids An array containing ids of each token being transferred (order and length must match values array) + * @param values An array containing amounts of each token being transferred (order and length must match ids array) + * @param data Additional data with no specified format + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} diff --git a/src/@openzeppelin/contracts/token/ERC20/IERC20.sol b/src/@openzeppelin/contracts/token/ERC20/IERC20.sol index 1f37231..abf307c 100644 --- a/src/@openzeppelin/contracts/token/ERC20/IERC20.sol +++ b/src/@openzeppelin/contracts/token/ERC20/IERC20.sol @@ -74,9 +74,5 @@ interface IERC20 { * * Emits a {Transfer} event. */ - function transferFrom( - address from, - address to, - uint256 amount - ) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); } diff --git a/src/@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol b/src/@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol index b3d0eca..add6bcb 100644 --- a/src/@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol +++ b/src/@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol @@ -33,15 +33,8 @@ interface IERC20Permit { * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP * section]. */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; /** * @dev Returns the current nonce for `owner`. This value must be diff --git a/src/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol b/src/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol index 580e5e8..3fefb45 100644 --- a/src/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol +++ b/src/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol @@ -19,20 +19,11 @@ import "../../../utils/Address.sol"; library SafeERC20 { using Address for address; - function safeTransfer( - IERC20 token, - address to, - uint256 value - ) internal { + function safeTransfer(IERC20 token, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); } - function safeTransferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) internal { + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); } @@ -43,11 +34,7 @@ library SafeERC20 { * Whenever possible, use {safeIncreaseAllowance} and * {safeDecreaseAllowance} instead. */ - function safeApprove( - IERC20 token, - address spender, - uint256 value - ) internal { + function safeApprove(IERC20 token, address spender, uint256 value) internal { // safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' @@ -58,20 +45,12 @@ library SafeERC20 { _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); } - function safeIncreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { uint256 newAllowance = token.allowance(address(this), spender) + value; _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); } - function safeDecreaseAllowance( - IERC20 token, - address spender, - uint256 value - ) internal { + function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { unchecked { uint256 oldAllowance = token.allowance(address(this), spender); require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); diff --git a/src/@openzeppelin/contracts/utils/Address.sol b/src/@openzeppelin/contracts/utils/Address.sol index cf55002..73e4393 100644 --- a/src/@openzeppelin/contracts/utils/Address.sol +++ b/src/@openzeppelin/contracts/utils/Address.sol @@ -60,7 +60,7 @@ library Address { function sendValue(address payable recipient, uint256 amount) internal { require(address(this).balance >= amount, "Address: insufficient balance"); - (bool success, ) = recipient.call{value: amount}(""); + (bool success,) = recipient.call{value: amount}(""); require(success, "Address: unable to send value, recipient may have reverted"); } @@ -92,11 +92,10 @@ library Address { * * _Available since v3.1._ */ - function functionCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { + function functionCall(address target, bytes memory data, string memory errorMessage) + internal + returns (bytes memory) + { return functionCallWithValue(target, data, 0, errorMessage); } @@ -111,11 +110,7 @@ library Address { * * _Available since v3.1._ */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value - ) internal returns (bytes memory) { + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); } @@ -125,12 +120,10 @@ library Address { * * _Available since v3.1._ */ - function functionCallWithValue( - address target, - bytes memory data, - uint256 value, - string memory errorMessage - ) internal returns (bytes memory) { + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) + internal + returns (bytes memory) + { require(address(this).balance >= value, "Address: insufficient balance for call"); require(isContract(target), "Address: call to non-contract"); @@ -154,11 +147,11 @@ library Address { * * _Available since v3.3._ */ - function functionStaticCall( - address target, - bytes memory data, - string memory errorMessage - ) internal view returns (bytes memory) { + function functionStaticCall(address target, bytes memory data, string memory errorMessage) + internal + view + returns (bytes memory) + { require(isContract(target), "Address: static call to non-contract"); (bool success, bytes memory returndata) = target.staticcall(data); @@ -181,11 +174,10 @@ library Address { * * _Available since v3.4._ */ - function functionDelegateCall( - address target, - bytes memory data, - string memory errorMessage - ) internal returns (bytes memory) { + function functionDelegateCall(address target, bytes memory data, string memory errorMessage) + internal + returns (bytes memory) + { require(isContract(target), "Address: delegate call to non-contract"); (bool success, bytes memory returndata) = target.delegatecall(data); @@ -198,11 +190,11 @@ library Address { * * _Available since v4.3._ */ - function verifyCallResult( - bool success, - bytes memory returndata, - string memory errorMessage - ) internal pure returns (bytes memory) { + function verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) + internal + pure + returns (bytes memory) + { if (success) { return returndata; } else { diff --git a/src/@openzeppelin/contracts/utils/StorageSlot.sol b/src/@openzeppelin/contracts/utils/StorageSlot.sol new file mode 100644 index 0000000..ba366f9 --- /dev/null +++ b/src/@openzeppelin/contracts/utils/StorageSlot.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +pragma solidity 0.8.27; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * ```solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + * + * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ + * _Available since v4.9 for `string`, `bytes`._ + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } +} diff --git a/src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol b/src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol index 71fdb51..25e1114 100644 --- a/src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol +++ b/src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol @@ -18,6 +18,7 @@ library ECDSA { InvalidSignatureLength, InvalidSignatureS, InvalidSignatureV // Deprecated in v4.8 + } function _throwError(RecoverError error) private pure { diff --git a/src/@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol b/src/@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol index a6478eb..b361fc9 100644 --- a/src/@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol +++ b/src/@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol @@ -23,9 +23,8 @@ library SignatureChecker { */ function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { (address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(hash, signature); - return - (error == ECDSA.RecoverError.NoError && recovered == signer) || - isValidERC1271SignatureNow(signer, hash, signature); + return (error == ECDSA.RecoverError.NoError && recovered == signer) + || isValidERC1271SignatureNow(signer, hash, signature); } /** @@ -35,16 +34,16 @@ library SignatureChecker { * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus * change through time. It could return true at block N and false at block N+1 (or the opposite). */ - function isValidERC1271SignatureNow( - address signer, - bytes32 hash, - bytes memory signature - ) internal view returns (bool) { - (bool success, bytes memory result) = signer.staticcall( - abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) + function isValidERC1271SignatureNow(address signer, bytes32 hash, bytes memory signature) + internal + view + returns (bool) + { + (bool success, bytes memory result) = + signer.staticcall(abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature)); + return ( + success && result.length >= 32 + && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector) ); - return (success && - result.length >= 32 && - abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); } } diff --git a/src/@openzeppelin/contracts/utils/introspection/ERC165Checker.sol b/src/@openzeppelin/contracts/utils/introspection/ERC165Checker.sol index 20c2f68..42010a4 100644 --- a/src/@openzeppelin/contracts/utils/introspection/ERC165Checker.sol +++ b/src/@openzeppelin/contracts/utils/introspection/ERC165Checker.sol @@ -22,9 +22,8 @@ library ERC165Checker { function supportsERC165(address account) internal view returns (bool) { // Any contract that implements ERC165 must explicitly indicate support of // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid - return - _supportsERC165Interface(account, type(IERC165).interfaceId) && - !_supportsERC165Interface(account, _INTERFACE_ID_INVALID); + return _supportsERC165Interface(account, type(IERC165).interfaceId) + && !_supportsERC165Interface(account, _INTERFACE_ID_INVALID); } /** diff --git a/src/@openzeppelin/contracts/utils/math/Math.sol b/src/@openzeppelin/contracts/utils/math/Math.sol index cb56185..1a2af13 100644 --- a/src/@openzeppelin/contracts/utils/math/Math.sol +++ b/src/@openzeppelin/contracts/utils/math/Math.sol @@ -11,6 +11,7 @@ library Math { Down, // Toward negative infinity Up, // Toward infinity Zero // Toward zero + } /** diff --git a/src/@permit2/interfaces/IAllowanceTransfer.sol b/src/@permit2/interfaces/IAllowanceTransfer.sol deleted file mode 100644 index 7c83608..0000000 --- a/src/@permit2/interfaces/IAllowanceTransfer.sol +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import {IEIP712} from "./IEIP712.sol"; - -/// @title AllowanceTransfer -/// @notice Handles ERC20 token permissions through signature based allowance setting and ERC20 token transfers by checking allowed amounts -/// @dev Requires user's token approval on the Permit2 contract -interface IAllowanceTransfer is IEIP712 { - /// @notice Thrown when an allowance on a token has expired. - /// @param deadline The timestamp at which the allowed amount is no longer valid - error AllowanceExpired(uint256 deadline); - - /// @notice Thrown when an allowance on a token has been depleted. - /// @param amount The maximum amount allowed - error InsufficientAllowance(uint256 amount); - - /// @notice Thrown when too many nonces are invalidated. - error ExcessiveInvalidation(); - - /// @notice Emits an event when the owner successfully invalidates an ordered nonce. - event NonceInvalidation( - address indexed owner, address indexed token, address indexed spender, uint48 newNonce, uint48 oldNonce - ); - - /// @notice Emits an event when the owner successfully sets permissions on a token for the spender. - event Approval( - address indexed owner, address indexed token, address indexed spender, uint160 amount, uint48 expiration - ); - - /// @notice Emits an event when the owner successfully sets permissions using a permit signature on a token for the spender. - event Permit( - address indexed owner, - address indexed token, - address indexed spender, - uint160 amount, - uint48 expiration, - uint48 nonce - ); - - /// @notice Emits an event when the owner sets the allowance back to 0 with the lockdown function. - event Lockdown(address indexed owner, address token, address spender); - - /// @notice The permit data for a token - struct PermitDetails { - // ERC20 token address - address token; - // the maximum amount allowed to spend - uint160 amount; - // timestamp at which a spender's token allowances become invalid - uint48 expiration; - // an incrementing value indexed per owner,token,and spender for each signature - uint48 nonce; - } - - /// @notice The permit message signed for a single token allowance - struct PermitSingle { - // the permit data for a single token alownce - PermitDetails details; - // address permissioned on the allowed tokens - address spender; - // deadline on the permit signature - uint256 sigDeadline; - } - - /// @notice The permit message signed for multiple token allowances - struct PermitBatch { - // the permit data for multiple token allowances - PermitDetails[] details; - // address permissioned on the allowed tokens - address spender; - // deadline on the permit signature - uint256 sigDeadline; - } - - /// @notice The saved permissions - /// @dev This info is saved per owner, per token, per spender and all signed over in the permit message - /// @dev Setting amount to type(uint160).max sets an unlimited approval - struct PackedAllowance { - // amount allowed - uint160 amount; - // permission expiry - uint48 expiration; - // an incrementing value indexed per owner,token,and spender for each signature - uint48 nonce; - } - - /// @notice A token spender pair. - struct TokenSpenderPair { - // the token the spender is approved - address token; - // the spender address - address spender; - } - - /// @notice Details for a token transfer. - struct AllowanceTransferDetails { - // the owner of the token - address from; - // the recipient of the token - address to; - // the amount of the token - uint160 amount; - // the token to be transferred - address token; - } - - /// @notice A mapping from owner address to token address to spender address to PackedAllowance struct, which contains details and conditions of the approval. - /// @notice The mapping is indexed in the above order see: allowance[ownerAddress][tokenAddress][spenderAddress] - /// @dev The packed slot holds the allowed amount, expiration at which the allowed amount is no longer valid, and current nonce thats updated on any signature based approvals. - function allowance(address user, address token, address spender) - external - view - returns (uint160 amount, uint48 expiration, uint48 nonce); - - /// @notice Approves the spender to use up to amount of the specified token up until the expiration - /// @param token The token to approve - /// @param spender The spender address to approve - /// @param amount The approved amount of the token - /// @param expiration The timestamp at which the approval is no longer valid - /// @dev The packed allowance also holds a nonce, which will stay unchanged in approve - /// @dev Setting amount to type(uint160).max sets an unlimited approval - function approve(address token, address spender, uint160 amount, uint48 expiration) external; - - /// @notice Permit a spender to a given amount of the owners token via the owner's EIP-712 signature - /// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce - /// @param owner The owner of the tokens being approved - /// @param permitSingle Data signed over by the owner specifying the terms of approval - /// @param signature The owner's signature over the permit data - function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external; - - /// @notice Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature - /// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce - /// @param owner The owner of the tokens being approved - /// @param permitBatch Data signed over by the owner specifying the terms of approval - /// @param signature The owner's signature over the permit data - function permit(address owner, PermitBatch memory permitBatch, bytes calldata signature) external; - - /// @notice Transfer approved tokens from one address to another - /// @param from The address to transfer from - /// @param to The address of the recipient - /// @param amount The amount of the token to transfer - /// @param token The token address to transfer - /// @dev Requires the from address to have approved at least the desired amount - /// of tokens to msg.sender. - function transferFrom(address from, address to, uint160 amount, address token) external; - - /// @notice Transfer approved tokens in a batch - /// @param transferDetails Array of owners, recipients, amounts, and tokens for the transfers - /// @dev Requires the from addresses to have approved at least the desired amount - /// of tokens to msg.sender. - function transferFrom(AllowanceTransferDetails[] calldata transferDetails) external; - - /// @notice Enables performing a "lockdown" of the sender's Permit2 identity - /// by batch revoking approvals - /// @param approvals Array of approvals to revoke. - function lockdown(TokenSpenderPair[] calldata approvals) external; - - /// @notice Invalidate nonces for a given (token, spender) pair - /// @param token The token to invalidate nonces for - /// @param spender The spender to invalidate nonces for - /// @param newNonce The new nonce to set. Invalidates all nonces less than it. - /// @dev Can't invalidate more than 2**16 nonces per transaction. - function invalidateNonces(address token, address spender, uint48 newNonce) external; -} diff --git a/src/@permit2/interfaces/IDAIPermit.sol b/src/@permit2/interfaces/IDAIPermit.sol deleted file mode 100644 index 11f8583..0000000 --- a/src/@permit2/interfaces/IDAIPermit.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -interface IDAIPermit { - /// @param holder The address of the token owner. - /// @param spender The address of the token spender. - /// @param nonce The owner's nonce, increases at each call to permit. - /// @param expiry The timestamp at which the permit is no longer valid. - /// @param allowed Boolean that sets approval amount, true for type(uint256).max and false for 0. - /// @param v Must produce valid secp256k1 signature from the owner along with r and s. - /// @param r Must produce valid secp256k1 signature from the owner along with v and s. - /// @param s Must produce valid secp256k1 signature from the owner along with r and v. - function permit( - address holder, - address spender, - uint256 nonce, - uint256 expiry, - bool allowed, - uint8 v, - bytes32 r, - bytes32 s - ) external; -} diff --git a/src/@permit2/interfaces/IEIP712.sol b/src/@permit2/interfaces/IEIP712.sol deleted file mode 100644 index cdce623..0000000 --- a/src/@permit2/interfaces/IEIP712.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -interface IEIP712 { - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/src/@permit2/interfaces/ISignatureTransfer.sol b/src/@permit2/interfaces/ISignatureTransfer.sol deleted file mode 100644 index bacf1fe..0000000 --- a/src/@permit2/interfaces/ISignatureTransfer.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import {IEIP712} from "./IEIP712.sol"; - -/// @title SignatureTransfer -/// @notice Handles ERC20 token transfers through signature based actions -/// @dev Requires user's token approval on the Permit2 contract -interface ISignatureTransfer is IEIP712 { - /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount - /// @param maxAmount The maximum amount a spender can request to transfer - error InvalidAmount(uint256 maxAmount); - - /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred - /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred - error LengthMismatch(); - - /// @notice Emits an event when the owner successfully invalidates an unordered nonce. - event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask); - - /// @notice The token and amount details for a transfer signed in the permit transfer signature - struct TokenPermissions { - // ERC20 token address - address token; - // the maximum amount that can be spent - uint256 amount; - } - - /// @notice The signed permit message for a single token transfer - struct PermitTransferFrom { - TokenPermissions permitted; - // a unique value for every token owner's signature to prevent signature replays - uint256 nonce; - // deadline on the permit signature - uint256 deadline; - } - - /// @notice Specifies the recipient address and amount for batched transfers. - /// @dev Recipients and amounts correspond to the index of the signed token permissions array. - /// @dev Reverts if the requested amount is greater than the permitted signed amount. - struct SignatureTransferDetails { - // recipient address - address to; - // spender requested amount - uint256 requestedAmount; - } - - /// @notice Used to reconstruct the signed permit message for multiple token transfers - /// @dev Do not need to pass in spender address as it is required that it is msg.sender - /// @dev Note that a user still signs over a spender address - struct PermitBatchTransferFrom { - // the tokens and corresponding amounts permitted for a transfer - TokenPermissions[] permitted; - // a unique value for every token owner's signature to prevent signature replays - uint256 nonce; - // deadline on the permit signature - uint256 deadline; - } - - /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection - /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order - /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce - /// @dev It returns a uint256 bitmap - /// @dev The index, or wordPosition is capped at type(uint248).max - function nonceBitmap(address, uint256) external view returns (uint256); - - /// @notice Transfers a token using a signed permit message - /// @dev Reverts if the requested amount is greater than the permitted signed amount - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails The spender's requested transfer details for the permitted token - /// @param signature The signature to verify - function permitTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes calldata signature - ) external; - - /// @notice Transfers a token using a signed permit message - /// @notice Includes extra data provided by the caller to verify signature over - /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition - /// @dev Reverts if the requested amount is greater than the permitted signed amount - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails The spender's requested transfer details for the permitted token - /// @param witness Extra data to include when checking the user signature - /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash - /// @param signature The signature to verify - function permitWitnessTransferFrom( - PermitTransferFrom memory permit, - SignatureTransferDetails calldata transferDetails, - address owner, - bytes32 witness, - string calldata witnessTypeString, - bytes calldata signature - ) external; - - /// @notice Transfers multiple tokens using a signed permit message - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails Specifies the recipient and requested amount for the token transfer - /// @param signature The signature to verify - function permitTransferFrom( - PermitBatchTransferFrom memory permit, - SignatureTransferDetails[] calldata transferDetails, - address owner, - bytes calldata signature - ) external; - - /// @notice Transfers multiple tokens using a signed permit message - /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition - /// @notice Includes extra data provided by the caller to verify signature over - /// @param permit The permit data signed over by the owner - /// @param owner The owner of the tokens to transfer - /// @param transferDetails Specifies the recipient and requested amount for the token transfer - /// @param witness Extra data to include when checking the user signature - /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash - /// @param signature The signature to verify - function permitWitnessTransferFrom( - PermitBatchTransferFrom memory permit, - SignatureTransferDetails[] calldata transferDetails, - address owner, - bytes32 witness, - string calldata witnessTypeString, - bytes calldata signature - ) external; - - /// @notice Invalidates the bits specified in mask for the bitmap at the word position - /// @dev The wordPos is maxed at type(uint248).max - /// @param wordPos A number to index the nonceBitmap at - /// @param mask A bitmap masked against msg.sender's current bitmap at the word position - function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; -} diff --git a/src/@permit2/libraries/Permit2Lib.sol b/src/@permit2/libraries/Permit2Lib.sol deleted file mode 100644 index 7b4f914..0000000 --- a/src/@permit2/libraries/Permit2Lib.sol +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import {ERC20} from "../../@solmate/tokens/ERC20.sol"; - -import {IDAIPermit} from "../interfaces/IDAIPermit.sol"; -import {IAllowanceTransfer} from "../interfaces/IAllowanceTransfer.sol"; -import {SafeCast160} from "./SafeCast160.sol"; - -/// @title Permit2Lib -/// @notice Enables efficient transfers and EIP-2612/DAI -/// permits for any token by falling back to Permit2. -library Permit2Lib { - using SafeCast160 for uint256; - /*////////////////////////////////////////////////////////////// - CONSTANTS - //////////////////////////////////////////////////////////////*/ - - /// @dev The unique EIP-712 domain domain separator for the DAI token contract. - bytes32 internal constant DAI_DOMAIN_SEPARATOR = 0xdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7; - - /// @dev The address for the WETH9 contract on Ethereum mainnet, encoded as a bytes32. - bytes32 internal constant WETH9_ADDRESS = 0x000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2; - - /// @dev The address of the Permit2 contract the library will use. - IAllowanceTransfer internal constant PERMIT2 = - IAllowanceTransfer(address(0x000000000022D473030F116dDEE9F6B43aC78BA3)); - - /// @notice Transfer a given amount of tokens from one user to another. - /// @param token The token to transfer. - /// @param from The user to transfer from. - /// @param to The user to transfer to. - /// @param amount The amount to transfer. - function transferFrom2(ERC20 token, address from, address to, uint256 amount) internal { - // Generate calldata for a standard transferFrom call. - bytes memory inputData = abi.encodeCall(ERC20.transferFrom, (from, to, amount)); - - bool success; // Call the token contract as normal, capturing whether it succeeded. - assembly { - success := - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(eq(mload(0), 1), iszero(returndatasize())), - // Counterintuitively, this call() must be positioned after the or() in the - // surrounding and() because and() evaluates its arguments from right to left. - // We use 0 and 32 to copy up to 32 bytes of return data into the first slot of scratch space. - call(gas(), token, 0, add(inputData, 32), mload(inputData), 0, 32) - ) - } - - // We'll fall back to using Permit2 if calling transferFrom on the token directly reverted. - if (!success) PERMIT2.transferFrom(from, to, amount.toUint160(), address(token)); - } - - /*////////////////////////////////////////////////////////////// - PERMIT LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Permit a user to spend a given amount of - /// another user's tokens via native EIP-2612 permit if possible, falling - /// back to Permit2 if native permit fails or is not implemented on the token. - /// @param token The token to permit spending. - /// @param owner The user to permit spending from. - /// @param spender The user to permit spending to. - /// @param amount The amount to permit spending. - /// @param deadline The timestamp after which the signature is no longer valid. - /// @param v Must produce valid secp256k1 signature from the owner along with r and s. - /// @param r Must produce valid secp256k1 signature from the owner along with v and s. - /// @param s Must produce valid secp256k1 signature from the owner along with r and v. - function permit2( - ERC20 token, - address owner, - address spender, - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) internal { - // Generate calldata for a call to DOMAIN_SEPARATOR on the token. - bytes memory inputData = abi.encodeWithSelector(ERC20.DOMAIN_SEPARATOR.selector); - - bool success; // Call the token contract as normal, capturing whether it succeeded. - bytes32 domainSeparator; // If the call succeeded, we'll capture the return value here. - - assembly { - // If the token is WETH9, we know it doesn't have a DOMAIN_SEPARATOR, and we'll skip this step. - // We make sure to mask the token address as its higher order bits aren't guaranteed to be clean. - if iszero(eq(and(token, 0xffffffffffffffffffffffffffffffffffffffff), WETH9_ADDRESS)) { - success := - and( - // Should resolve false if its not 32 bytes or its first word is 0. - and(iszero(iszero(mload(0))), eq(returndatasize(), 32)), - // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. - // Counterintuitively, this call must be positioned second to the and() call in the - // surrounding and() call or else returndatasize() will be zero during the computation. - // We send a maximum of 5000 gas to prevent tokens with fallbacks from using a ton of gas. - // which should be plenty to allow tokens to fetch their DOMAIN_SEPARATOR from storage, etc. - staticcall(5000, token, add(inputData, 32), mload(inputData), 0, 32) - ) - - domainSeparator := mload(0) // Copy the return value into the domainSeparator variable. - } - } - - // If the call to DOMAIN_SEPARATOR succeeded, try using permit on the token. - if (success) { - // We'll use DAI's special permit if it's DOMAIN_SEPARATOR matches, - // otherwise we'll just encode a call to the standard permit function. - inputData = domainSeparator == DAI_DOMAIN_SEPARATOR - ? abi.encodeCall(IDAIPermit.permit, (owner, spender, token.nonces(owner), deadline, true, v, r, s)) - : abi.encodeCall(ERC20.permit, (owner, spender, amount, deadline, v, r, s)); - - assembly { - success := call(gas(), token, 0, add(inputData, 32), mload(inputData), 0, 0) - } - } - - if (!success) { - // If the initial DOMAIN_SEPARATOR call on the token failed or a - // subsequent call to permit failed, fall back to using Permit2. - simplePermit2(token, owner, spender, amount, deadline, v, r, s); - } - } - - /// @notice Simple unlimited permit on the Permit2 contract. - /// @param token The token to permit spending. - /// @param owner The user to permit spending from. - /// @param spender The user to permit spending to. - /// @param amount The amount to permit spending. - /// @param deadline The timestamp after which the signature is no longer valid. - /// @param v Must produce valid secp256k1 signature from the owner along with r and s. - /// @param r Must produce valid secp256k1 signature from the owner along with v and s. - /// @param s Must produce valid secp256k1 signature from the owner along with r and v. - function simplePermit2( - ERC20 token, - address owner, - address spender, - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) internal { - (,, uint48 nonce) = PERMIT2.allowance(owner, address(token), spender); - - PERMIT2.permit( - owner, - IAllowanceTransfer.PermitSingle({ - details: IAllowanceTransfer.PermitDetails({ - token: address(token), - amount: amount.toUint160(), - // Use an unlimited expiration because it most - // closely mimics how a standard approval works. - expiration: type(uint48).max, - nonce: nonce - }), - spender: spender, - sigDeadline: deadline - }), - bytes.concat(r, s, bytes1(v)) - ); - } -} diff --git a/src/@permit2/libraries/PermitHash.sol b/src/@permit2/libraries/PermitHash.sol deleted file mode 100644 index cdc0172..0000000 --- a/src/@permit2/libraries/PermitHash.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import {IAllowanceTransfer} from "../interfaces/IAllowanceTransfer.sol"; -import {ISignatureTransfer} from "../interfaces/ISignatureTransfer.sol"; - -library PermitHash { - bytes32 public constant _PERMIT_DETAILS_TYPEHASH = - keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"); - - bytes32 public constant _PERMIT_SINGLE_TYPEHASH = keccak256( - "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" - ); - - bytes32 public constant _PERMIT_BATCH_TYPEHASH = keccak256( - "PermitBatch(PermitDetails[] details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" - ); - - bytes32 public constant _TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); - - bytes32 public constant _PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( - "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" - ); - - bytes32 public constant _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH = keccak256( - "PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" - ); - - string public constant _TOKEN_PERMISSIONS_TYPESTRING = "TokenPermissions(address token,uint256 amount)"; - - string public constant _PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB = - "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,"; - - string public constant _PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB = - "PermitBatchWitnessTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline,"; - - function hash(IAllowanceTransfer.PermitSingle memory permitSingle) internal pure returns (bytes32) { - bytes32 permitHash = _hashPermitDetails(permitSingle.details); - return - keccak256(abi.encode(_PERMIT_SINGLE_TYPEHASH, permitHash, permitSingle.spender, permitSingle.sigDeadline)); - } - - function hash(IAllowanceTransfer.PermitBatch memory permitBatch) internal pure returns (bytes32) { - uint256 numPermits = permitBatch.details.length; - bytes32[] memory permitHashes = new bytes32[](numPermits); - for (uint256 i = 0; i < numPermits; ++i) { - permitHashes[i] = _hashPermitDetails(permitBatch.details[i]); - } - return keccak256( - abi.encode( - _PERMIT_BATCH_TYPEHASH, - keccak256(abi.encodePacked(permitHashes)), - permitBatch.spender, - permitBatch.sigDeadline - ) - ); - } - - function hash(ISignatureTransfer.PermitTransferFrom memory permit) internal view returns (bytes32) { - bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted); - return keccak256( - abi.encode(_PERMIT_TRANSFER_FROM_TYPEHASH, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline) - ); - } - - function hash(ISignatureTransfer.PermitBatchTransferFrom memory permit) internal view returns (bytes32) { - uint256 numPermitted = permit.permitted.length; - bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted); - - for (uint256 i = 0; i < numPermitted; ++i) { - tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]); - } - - return keccak256( - abi.encode( - _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH, - keccak256(abi.encodePacked(tokenPermissionHashes)), - msg.sender, - permit.nonce, - permit.deadline - ) - ); - } - - function hashWithWitness( - ISignatureTransfer.PermitTransferFrom memory permit, - bytes32 witness, - string calldata witnessTypeString - ) internal view returns (bytes32) { - bytes32 typeHash = keccak256(abi.encodePacked(_PERMIT_TRANSFER_FROM_WITNESS_TYPEHASH_STUB, witnessTypeString)); - - bytes32 tokenPermissionsHash = _hashTokenPermissions(permit.permitted); - return keccak256(abi.encode(typeHash, tokenPermissionsHash, msg.sender, permit.nonce, permit.deadline, witness)); - } - - function hashWithWitness( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - bytes32 witness, - string calldata witnessTypeString - ) internal view returns (bytes32) { - bytes32 typeHash = - keccak256(abi.encodePacked(_PERMIT_BATCH_WITNESS_TRANSFER_FROM_TYPEHASH_STUB, witnessTypeString)); - - uint256 numPermitted = permit.permitted.length; - bytes32[] memory tokenPermissionHashes = new bytes32[](numPermitted); - - for (uint256 i = 0; i < numPermitted; ++i) { - tokenPermissionHashes[i] = _hashTokenPermissions(permit.permitted[i]); - } - - return keccak256( - abi.encode( - typeHash, - keccak256(abi.encodePacked(tokenPermissionHashes)), - msg.sender, - permit.nonce, - permit.deadline, - witness - ) - ); - } - - function _hashPermitDetails(IAllowanceTransfer.PermitDetails memory details) private pure returns (bytes32) { - return keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, details)); - } - - function _hashTokenPermissions(ISignatureTransfer.TokenPermissions memory permitted) - private - pure - returns (bytes32) - { - return keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permitted)); - } -} diff --git a/src/@permit2/libraries/SafeCast160.sol b/src/@permit2/libraries/SafeCast160.sol deleted file mode 100644 index dd8f332..0000000 --- a/src/@permit2/libraries/SafeCast160.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -library SafeCast160 { - /// @notice Thrown when a valude greater than type(uint160).max is cast to uint160 - error UnsafeCast(); - - /// @notice Safely casts uint256 to uint160 - /// @param value The uint256 to be cast - function toUint160(uint256 value) internal pure returns (uint160) { - if (value > type(uint160).max) revert UnsafeCast(); - return uint160(value); - } -} diff --git a/src/@permit2/libraries/SignatureVerification.sol b/src/@permit2/libraries/SignatureVerification.sol deleted file mode 100644 index d9a015e..0000000 --- a/src/@permit2/libraries/SignatureVerification.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "../../@openzeppelin/contracts/interfaces/IERC1271.sol"; - -library SignatureVerification { - /// @notice Thrown when the passed in signature is not a valid length - error InvalidSignatureLength(); - - /// @notice Thrown when the recovered signer is equal to the zero address - error InvalidSignature(); - - /// @notice Thrown when the recovered signer does not equal the claimedSigner - error InvalidSigner(); - - /// @notice Thrown when the recovered contract signature is incorrect - error InvalidContractSignature(); - - bytes32 constant UPPER_BIT_MASK = (0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); - - function verify(bytes calldata signature, bytes32 hash, address claimedSigner) internal view { - bytes32 r; - bytes32 s; - uint8 v; - - if (claimedSigner.code.length == 0) { - if (signature.length == 65) { - (r, s) = abi.decode(signature, (bytes32, bytes32)); - v = uint8(signature[64]); - } else if (signature.length == 64) { - // EIP-2098 - bytes32 vs; - (r, vs) = abi.decode(signature, (bytes32, bytes32)); - s = vs & UPPER_BIT_MASK; - v = uint8(uint256(vs >> 255)) + 27; - } else { - revert InvalidSignatureLength(); - } - address signer = ecrecover(hash, v, r, s); - if (signer == address(0)) revert InvalidSignature(); - if (signer != claimedSigner) revert InvalidSigner(); - } else { - bytes4 magicValue = IERC1271(claimedSigner).isValidSignature(hash, signature); - if (magicValue != IERC1271.isValidSignature.selector) revert InvalidContractSignature(); - } - } -} diff --git a/src/@solmate/tokens/ERC20.sol b/src/@solmate/tokens/ERC20.sol index de0594d..783ee74 100644 --- a/src/@solmate/tokens/ERC20.sol +++ b/src/@solmate/tokens/ERC20.sol @@ -48,11 +48,7 @@ abstract contract ERC20 { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor( - string memory _name, - string memory _symbol, - uint8 _decimals - ) { + constructor(string memory _name, string memory _symbol, uint8 _decimals) { name = _name; symbol = _symbol; decimals = _decimals; @@ -87,11 +83,7 @@ abstract contract ERC20 { return true; } - function transferFrom( - address from, - address to, - uint256 amount - ) public virtual returns (bool) { + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; @@ -113,15 +105,10 @@ abstract contract ERC20 { EIP-2612 LOGIC //////////////////////////////////////////////////////////////*/ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + virtual + { require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); // Unchecked because the only math done is incrementing @@ -164,16 +151,15 @@ abstract contract ERC20 { } function computeDomainSeparator() internal view virtual returns (bytes32) { - return - keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name)), - keccak256("1"), - block.chainid, - address(this) - ) - ); + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); } /*////////////////////////////////////////////////////////////// diff --git a/src/access/P2pOperator2Step.sol b/src/access/P2pOperator2Step.sol index 223917a..c03bae0 100644 --- a/src/access/P2pOperator2Step.sol +++ b/src/access/P2pOperator2Step.sol @@ -27,7 +27,9 @@ import {P2pOperator} from "./P2pOperator.sol"; abstract contract P2pOperator2Step is P2pOperator { address private s_pendingP2pOperator; - event P2pOperator2Step__P2pOperatorTransferStarted(address indexed _previousP2pOperator, address indexed _newP2pOperator); + event P2pOperator2Step__P2pOperatorTransferStarted( + address indexed _previousP2pOperator, address indexed _newP2pOperator + ); /** * @dev Returns the address of the pending P2pOperator. diff --git a/src/adapters/ethena/IStakedUSDe.sol b/src/adapters/ethena/IStakedUSDe.sol deleted file mode 100644 index 30e5b53..0000000 --- a/src/adapters/ethena/IStakedUSDe.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2025 P2P Validator -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; - -import "../../@openzeppelin/contracts/interfaces/IERC4626.sol"; - -interface IStakedUSDe is IERC4626 { - /// @notice redeem assets and starts a cooldown to claim the converted underlying asset - /// @param assets assets to redeem - function cooldownAssets(uint256 assets) external returns (uint256 shares); - - /// @notice redeem shares into assets and starts a cooldown to claim the converted underlying asset - /// @param shares shares to redeem - function cooldownShares(uint256 shares) external returns (uint256 assets); - - /// @notice Claim the staking amount after the cooldown has finished. The address can only retire the full amount of assets. - /// @dev unstake can be called after cooldown have been set to 0, to let accounts to be able to claim remaining assets locked at Silo - /// @param receiver Address to send the assets by the staker - function unstake(address receiver) external; -} diff --git a/src/adapters/ethena/p2pEthenaProxy/IP2pEthenaProxy.sol b/src/adapters/ethena/p2pEthenaProxy/IP2pEthenaProxy.sol deleted file mode 100644 index e10ac6d..0000000 --- a/src/adapters/ethena/p2pEthenaProxy/IP2pEthenaProxy.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2025 P2P Validator -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; - -interface IP2pEthenaProxy { - /// @notice redeem assets and starts a cooldown to claim the converted underlying asset - /// @param _assets assets to redeem - function cooldownAssets(uint256 _assets) external returns (uint256 shares); - - /// @notice redeem shares into assets and starts a cooldown to claim the converted underlying asset - /// @param _shares shares to redeem - function cooldownShares(uint256 _shares) external returns (uint256 assets); - - /// @notice withdraw assets after cooldown has elapsed - function withdrawAfterCooldown() external; - - /// @notice withdraw assets without cooldown if cooldownDuration has been set to 0 on StakedUSDeV2 - /// @param _assets assets to redeem - function withdrawWithoutCooldown(uint256 _assets) external; - - /// @notice withdraw shares without cooldown if cooldownDuration has been set to 0 on StakedUSDeV2 - /// @param _shares shares to redeem - function redeemWithoutCooldown(uint256 _shares) external; -} diff --git a/src/adapters/ethena/p2pEthenaProxy/P2pEthenaProxy.sol b/src/adapters/ethena/p2pEthenaProxy/P2pEthenaProxy.sol deleted file mode 100644 index 39ee2cd..0000000 --- a/src/adapters/ethena/p2pEthenaProxy/P2pEthenaProxy.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-FileCopyrightText: 2025 P2P Validator -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; - -import "../../../p2pYieldProxy/P2pYieldProxy.sol"; -import "../IStakedUSDe.sol"; -import "./IP2pEthenaProxy.sol"; - -contract P2pEthenaProxy is P2pYieldProxy, IP2pEthenaProxy { - using SafeERC20 for IERC20; - - /// @dev USDe address - address internal immutable i_USDe; - - /// @notice Constructor for P2pEthenaProxy - /// @param _factory Factory address - /// @param _p2pTreasury P2pTreasury address - /// @param _stakedUSDeV2 StakedUSDeV2 address - /// @param _USDe USDe address - constructor( - address _factory, - address _p2pTreasury, - address _stakedUSDeV2, - address _USDe - ) P2pYieldProxy(_factory, _p2pTreasury, _stakedUSDeV2) { - i_USDe = _USDe; - } - - /// @inheritdoc IP2pYieldProxy - function deposit( - IAllowanceTransfer.PermitSingle calldata _permitSingleForP2pYieldProxy, - bytes calldata _permit2SignatureForP2pYieldProxy - ) external { - _deposit( - abi.encodeCall( - IERC4626.deposit, - (uint256(_permitSingleForP2pYieldProxy.details.amount), address(this)) - ), - _permitSingleForP2pYieldProxy, - _permit2SignatureForP2pYieldProxy, - false - ); - } - - /// @inheritdoc IP2pEthenaProxy - function cooldownAssets(uint256 _assets) - external - onlyClient - returns (uint256 shares) { - return IStakedUSDe(i_yieldProtocolAddress).cooldownAssets(_assets); - } - - /// @inheritdoc IP2pEthenaProxy - function cooldownShares(uint256 _shares) - external - onlyClient - returns (uint256 assets) { - return IStakedUSDe(i_yieldProtocolAddress).cooldownShares(_shares); - } - - /// @inheritdoc IP2pEthenaProxy - function withdrawAfterCooldown() external { - _withdraw( - i_USDe, - abi.encodeCall( - IStakedUSDe.unstake, - (address(this)) - ) - ); - } - - /// @inheritdoc IP2pEthenaProxy - function withdrawWithoutCooldown(uint256 _assets) external { - _withdraw( - i_USDe, - abi.encodeCall( - IERC4626.withdraw, - (_assets, address(this), address(this)) - ) - ); - } - - /// @inheritdoc IP2pEthenaProxy - function redeemWithoutCooldown(uint256 _shares) external { - _withdraw( - i_USDe, - abi.encodeCall( - IERC4626.redeem, - (_shares, address(this), address(this)) - ) - ); - } - - /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(P2pYieldProxy) returns (bool) { - return interfaceId == type(IP2pEthenaProxy).interfaceId || - super.supportsInterface(interfaceId); - } -} diff --git a/src/adapters/ethena/p2pEthenaProxyFactory/IP2pEthenaProxyFactory.sol b/src/adapters/ethena/p2pEthenaProxyFactory/IP2pEthenaProxyFactory.sol deleted file mode 100644 index 9bb8253..0000000 --- a/src/adapters/ethena/p2pEthenaProxyFactory/IP2pEthenaProxyFactory.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2025 P2P Validator -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; -import "../../../@permit2/interfaces/IAllowanceTransfer.sol"; - -/// @dev External interface of P2pEthenaProxyFactory -interface IP2pEthenaProxyFactory { -} diff --git a/src/adapters/ethena/p2pEthenaProxyFactory/P2pEthenaProxyFactory.sol b/src/adapters/ethena/p2pEthenaProxyFactory/P2pEthenaProxyFactory.sol deleted file mode 100644 index 05c4a7e..0000000 --- a/src/adapters/ethena/p2pEthenaProxyFactory/P2pEthenaProxyFactory.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2025 P2P Validator -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; - -import "../../../@permit2/interfaces/IAllowanceTransfer.sol"; -import "../../../p2pYieldProxyFactory/P2pYieldProxyFactory.sol"; -import "../p2pEthenaProxy/P2pEthenaProxy.sol"; -import "./IP2pEthenaProxyFactory.sol"; -import {IERC4626} from "../../../@openzeppelin/contracts/interfaces/IERC4626.sol"; - -/// @title Entry point for depositing into Ethena with P2P.org -contract P2pEthenaProxyFactory is P2pYieldProxyFactory, IP2pEthenaProxyFactory { - - /// @notice Constructor for P2pEthenaProxyFactory - /// @param _p2pSigner The P2pSigner address - /// @param _p2pTreasury The P2pTreasury address - /// @param _stakedUSDeV2 StakedUSDeV2 - /// @param _USDe USDe address - constructor( - address _p2pSigner, - address _p2pTreasury, - address _stakedUSDeV2, - address _USDe - ) P2pYieldProxyFactory(_p2pSigner) { - i_referenceP2pYieldProxy = new P2pEthenaProxy( - address(this), - _p2pTreasury, - _stakedUSDeV2, - _USDe - ); - } - - /// @inheritdoc ERC165 - function supportsInterface(bytes4 interfaceId) public view virtual override(P2pYieldProxyFactory) returns (bool) { - return interfaceId == type(IP2pEthenaProxyFactory).interfaceId || - super.supportsInterface(interfaceId); - } -} diff --git a/src/adapters/superform/DataTypes.sol b/src/adapters/superform/DataTypes.sol new file mode 100644 index 0000000..65bb2cf --- /dev/null +++ b/src/adapters/superform/DataTypes.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.27; + +/// @dev contains all the common struct and enums used for data communication between chains. + +/// @dev There are two transaction types in Superform Protocol +enum TransactionType { + DEPOSIT, + WITHDRAW +} + +/// @dev Message types can be INIT, RETURN (for successful Deposits) and FAIL (for failed withdraws) +enum CallbackType { + INIT, + RETURN, + FAIL +} + +/// @dev Payloads are stored, updated (deposits) or processed (finalized) +enum PayloadState { + STORED, + UPDATED, + PROCESSED +} + +/// @dev contains all the common struct used for interchain token transfers. +struct LiqRequest { + /// @dev generated data + bytes txData; + /// @dev input token for deposits, desired output token on target liqDstChainId for withdraws. Must be set for + /// txData to be updated on destination for withdraws + address token; + /// @dev intermediary token on destination. Relevant for xChain deposits where a destination swap is needed for + /// validation purposes + address interimToken; + /// @dev what bridge to use to move tokens + uint8 bridgeId; + /// @dev dstChainId = liqDstchainId for deposits. For withdraws it is the target chain id for where the underlying + /// is to be delivered + uint64 liqDstChainId; + /// @dev currently this amount is used as msg.value in the txData call. + uint256 nativeAmount; +} + +/// @dev main struct that holds required multi vault data for an action +struct MultiVaultSFData { + // superformids must have same destination. Can have different underlyings + uint256[] superformIds; + uint256[] amounts; // on deposits, amount of token to deposit on dst, on withdrawals, superpositions to burn + uint256[] outputAmounts; // on deposits, amount of shares to receive, on withdrawals, amount of assets to receive + uint256[] maxSlippages; + LiqRequest[] liqRequests; // if length = 1; amount = sum(amounts) | else amounts must match the amounts being sent + bytes permit2data; + bool[] hasDstSwaps; + bool[] retain4626s; // if true, we don't mint SuperPositions, and send the 4626 back to the user instead + address receiverAddress; + /// this address must always be an EOA otherwise funds may be lost + address receiverAddressSP; + /// this address can be a EOA or a contract that implements onERC1155Receiver. must always be set for deposits + bytes extraFormData; // extraFormData +} + +/// @dev main struct that holds required single vault data for an action +struct SingleVaultSFData { + // superformids must have same destination. Can have different underlyings + uint256 superformId; + uint256 amount; + uint256 outputAmount; // on deposits, amount of shares to receive, on withdrawals, amount of assets to receive + uint256 maxSlippage; + LiqRequest liqRequest; // if length = 1; amount = sum(amounts)| else amounts must match the amounts being sent + bytes permit2data; + bool hasDstSwap; + bool retain4626; // if true, we don't mint SuperPositions, and send the 4626 back to the user instead + address receiverAddress; + /// this address must always be an EOA otherwise funds may be lost + address receiverAddressSP; + /// this address can be a EOA or a contract that implements onERC1155Receiver. must always be set for deposits + bytes extraFormData; // extraFormData +} + +/// @dev overarching struct for multiDst requests with multi vaults +struct MultiDstMultiVaultStateReq { + uint8[][] ambIds; + uint64[] dstChainIds; + MultiVaultSFData[] superformsData; +} + +/// @dev overarching struct for single cross chain requests with multi vaults +struct SingleXChainMultiVaultStateReq { + uint8[] ambIds; + uint64 dstChainId; + MultiVaultSFData superformsData; +} + +/// @dev overarching struct for multiDst requests with single vaults +struct MultiDstSingleVaultStateReq { + uint8[][] ambIds; + uint64[] dstChainIds; + SingleVaultSFData[] superformsData; +} + +/// @dev overarching struct for single cross chain requests with single vaults +struct SingleXChainSingleVaultStateReq { + uint8[] ambIds; + uint64 dstChainId; + SingleVaultSFData superformData; +} + +/// @dev overarching struct for single direct chain requests with single vaults +struct SingleDirectSingleVaultStateReq { + SingleVaultSFData superformData; +} + +/// @dev overarching struct for single direct chain requests with multi vaults +struct SingleDirectMultiVaultStateReq { + MultiVaultSFData superformData; +} + +/// @dev struct for SuperRouter with re-arranged data for the message (contains the payloadId) +/// @dev realize that receiverAddressSP is not passed, only needed on source chain to mint +struct InitMultiVaultData { + uint256 payloadId; + uint256[] superformIds; + uint256[] amounts; + uint256[] outputAmounts; + uint256[] maxSlippages; + LiqRequest[] liqData; + bool[] hasDstSwaps; + bool[] retain4626s; + address receiverAddress; + bytes extraFormData; +} + +/// @dev struct for SuperRouter with re-arranged data for the message (contains the payloadId) +struct InitSingleVaultData { + uint256 payloadId; + uint256 superformId; + uint256 amount; + uint256 outputAmount; + uint256 maxSlippage; + LiqRequest liqData; + bool hasDstSwap; + bool retain4626; + address receiverAddress; + bytes extraFormData; +} + +/// @dev struct for Emergency Queue +struct QueuedWithdrawal { + address receiverAddress; + uint256 superformId; + uint256 amount; + uint256 srcPayloadId; + bool isProcessed; +} + +/// @dev all statuses of the timelock payload +enum TimelockStatus { + UNAVAILABLE, + PENDING, + PROCESSED +} + +/// @dev holds information about the timelock payload +struct TimelockPayload { + uint8 isXChain; + uint64 srcChainId; + uint256 lockedTill; + InitSingleVaultData data; + TimelockStatus status; +} + +/// @dev struct that contains the type of transaction, callback flags and other identification, as well as the vaults +/// data in params +struct AMBMessage { + uint256 txInfo; // tight packing of TransactionType txType, CallbackType flag if multi/single vault, registry id, + // srcSender and srcChainId + bytes params; // decoding txInfo will point to the right datatype of params. Refer PayloadHelper.sol +} + +/// @dev struct that contains the information required for broadcasting changes +struct BroadcastMessage { + bytes target; + bytes32 messageType; + bytes message; +} + +/// @dev struct that contains info on returned data from destination +struct ReturnMultiData { + uint256 payloadId; + uint256[] superformIds; + uint256[] amounts; +} + +/// @dev struct that contains info on returned data from destination +struct ReturnSingleData { + uint256 payloadId; + uint256 superformId; + uint256 amount; +} + +/// @dev struct that contains the data on the fees to pay to the AMBs +struct AMBExtraData { + uint256[] gasPerAMB; + bytes[] extraDataPerAMB; +} + +/// @dev struct that contains the data on the fees to pay to the AMBs on broadcasts +struct BroadCastAMBExtraData { + uint256[] gasPerDst; + bytes[] extraDataPerDst; +} diff --git a/src/adapters/superform/IBaseForm.sol b/src/adapters/superform/IBaseForm.sol new file mode 100644 index 0000000..8811d7b --- /dev/null +++ b/src/adapters/superform/IBaseForm.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.27; + +import "../../@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "../../@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "./DataTypes.sol"; + +/// @title IBaseForm +/// @dev Interface for BaseForm +/// @author ZeroPoint Labs +interface IBaseForm is IERC165 { + ////////////////////////////////////////////////////////////// + // EVENTS // + ////////////////////////////////////////////////////////////// + + /// @dev is emitted when a new vault is added by the admin. + event VaultAdded(uint256 indexed id, IERC4626 indexed vault); + + /// @dev is emitted when a payload is processed by the destination contract. + event Processed( + uint64 indexed srcChainID, + uint64 indexed dstChainId, + uint256 indexed srcPayloadId, + uint256 amount, + address vault + ); + + /// @dev is emitted when an emergency withdrawal is processed + event EmergencyWithdrawalProcessed(address indexed refundAddress, uint256 indexed amount); + + /// @dev is emitted when dust is forwarded to the paymaster + event FormDustForwardedToPaymaster(address indexed token, uint256 indexed amount); + + ////////////////////////////////////////////////////////////// + // EXTERNAL VIEW FUNCTIONS // + ////////////////////////////////////////////////////////////// + + /// @notice get Superform name of the ERC20 vault representation + /// @return The ERC20 name + function superformYieldTokenName() external view returns (string memory); + + /// @notice get Superform symbol of the ERC20 vault representation + /// @return The ERC20 symbol + function superformYieldTokenSymbol() external view returns (string memory); + + /// @notice get the state registry id associated with the vault + function getStateRegistryId() external view returns (uint8); + + /// @notice Returns the vault address + /// @return The address of the vault + function getVaultAddress() external view returns (address); + + /// @notice Returns the vault address + /// @return The address of the vault asset + function getVaultAsset() external view returns (address); + + /// @notice Returns the name of the vault. + /// @return The name of the vault + function getVaultName() external view returns (string memory); + + /// @notice Returns the symbol of a vault. + /// @return The symbol associated with a vault + function getVaultSymbol() external view returns (string memory); + + /// @notice Returns the number of decimals in a vault for accounting purposes + /// @return The number of decimals in the vault balance + function getVaultDecimals() external view returns (uint256); + + /// @notice Returns the amount of underlying tokens each share of a vault is worth. + /// @return The pricePerVaultShare value + function getPricePerVaultShare() external view returns (uint256); + + /// @notice Returns the amount of vault shares owned by the form. + /// @return The form's vault share balance + function getVaultShareBalance() external view returns (uint256); + + /// @notice get the total amount of underlying managed in the ERC4626 vault + function getTotalAssets() external view returns (uint256); + + /// @notice get the total amount of unredeemed vault shares in circulation + function getTotalSupply() external view returns (uint256); + + /// @notice get the total amount of assets received if shares are actually redeemed + /// @notice https://eips.ethereum.org/EIPS/eip-4626 + function getPreviewPricePerVaultShare() external view returns (uint256); + + /// @dev API may need to know state of funds deployed + function previewDepositTo(uint256 assets_) external view returns (uint256); + + /// @notice positionBalance() -> .vaultIds&destAmounts + /// @return how much of an asset + interest (accrued) is to withdraw from the Vault + function previewWithdrawFrom(uint256 assets_) external view returns (uint256); + + /// @dev API may need to know state of funds deployed + function previewRedeemFrom(uint256 shares_) external view returns (uint256); + + ////////////////////////////////////////////////////////////// + // EXTERNAL WRITE FUNCTIONS // + ////////////////////////////////////////////////////////////// + + /// @dev process same chain id deposits + /// @param singleVaultData_ A bytes representation containing all the data required to make a form action + /// @param srcSender_ The address of the sender of the transaction + /// @return shares The amount of vault shares received + function directDepositIntoVault(InitSingleVaultData memory singleVaultData_, address srcSender_) + external + payable + returns (uint256 shares); + + /// @dev process same chain id deposits + /// @param singleVaultData_ A bytes representation containing all the data required to make a form action + /// @param srcSender_ The address of the sender of the transaction + /// @param srcChainId_ The chain id of the source chain + /// @return shares The amount of vault shares received + /// @dev is shares is `0` then no further action/acknowledgement needs to be sent + function xChainDepositIntoVault(InitSingleVaultData memory singleVaultData_, address srcSender_, uint64 srcChainId_) + external + returns (uint256 shares); + + /// @dev process withdrawal of asset from a vault + /// @param singleVaultData_ A bytes representation containing all the data required to make a form action + /// @param srcSender_ The address of the sender of the transaction + /// @return assets The amount of assets received + function directWithdrawFromVault(InitSingleVaultData memory singleVaultData_, address srcSender_) + external + returns (uint256 assets); + + /// @dev process withdrawal of asset from a vault + /// @param singleVaultData_ A bytes representation containing all the data required to make a form action + /// @param srcSender_ The address of the sender of the transaction + /// @param srcChainId_ The chain id of the source chain + /// @return assets The amount of assets received + function xChainWithdrawFromVault( + InitSingleVaultData memory singleVaultData_, + address srcSender_, + uint64 srcChainId_ + ) external returns (uint256 assets); + + /// @dev process withdrawal of shares if form is paused + /// @param receiverAddress_ The address to refund the shares to + /// @param amount_ The amount of vault shares to refund + function emergencyWithdraw(address receiverAddress_, uint256 amount_) external; + + /// @dev moves all dust in the contract to Paymaster contract + /// @param token_ The address of the token to forward + function forwardDustToPaymaster(address token_) external; +} diff --git a/src/adapters/superform/IBaseRouter.sol b/src/adapters/superform/IBaseRouter.sol new file mode 100644 index 0000000..abf721b --- /dev/null +++ b/src/adapters/superform/IBaseRouter.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.27; + +import "./DataTypes.sol"; + +/// @title IBaseRouter +/// @dev Interface for abstract BaseRouter +/// @author Zeropoint Labs +interface IBaseRouter { + ////////////////////////////////////////////////////////////// + // EXTERNAL WRITE FUNCTIONS // + ////////////////////////////////////////////////////////////// + + /// @dev Performs single direct x single vault deposits + /// @param req_ is the request object containing all the necessary data for the action + function singleDirectSingleVaultDeposit(SingleDirectSingleVaultStateReq memory req_) external payable; + + /// @dev Performs single xchain destination x single vault deposits + /// @param req_ is the request object containing all the necessary data for the action + function singleXChainSingleVaultDeposit(SingleXChainSingleVaultStateReq memory req_) external payable; + + /// @dev Performs single direct x multi vault deposits + /// @param req_ is the request object containing all the necessary data for the action + function singleDirectMultiVaultDeposit(SingleDirectMultiVaultStateReq memory req_) external payable; + + /// @dev Performs single destination x multi vault deposits + /// @param req_ is the request object containing all the necessary data for the action + function singleXChainMultiVaultDeposit(SingleXChainMultiVaultStateReq memory req_) external payable; + + /// @dev Performs multi destination x single vault deposits + /// @param req_ is the request object containing all the necessary data for the action + function multiDstSingleVaultDeposit(MultiDstSingleVaultStateReq calldata req_) external payable; + + /// @dev Performs multi destination x multi vault deposits + /// @param req_ is the request object containing all the necessary data for the action + function multiDstMultiVaultDeposit(MultiDstMultiVaultStateReq calldata req_) external payable; + + /// @dev Performs single direct x single vault withdraws + /// @param req_ is the request object containing all the necessary data for the action + function singleDirectSingleVaultWithdraw(SingleDirectSingleVaultStateReq memory req_) external payable; + + /// @dev Performs single xchain destination x single vault withdraws + /// @param req_ is the request object containing all the necessary data for the action + function singleXChainSingleVaultWithdraw(SingleXChainSingleVaultStateReq memory req_) external payable; + + /// @dev Performs single direct x multi vault withdraws + /// @param req_ is the request object containing all the necessary data for the action + function singleDirectMultiVaultWithdraw(SingleDirectMultiVaultStateReq memory req_) external payable; + + /// @dev Performs single destination x multi vault withdraws + /// @param req_ is the request object containing all the necessary data for the action + function singleXChainMultiVaultWithdraw(SingleXChainMultiVaultStateReq memory req_) external payable; + + /// @dev Performs multi destination x single vault withdraws + /// @param req_ is the request object containing all the necessary data for the action + function multiDstSingleVaultWithdraw(MultiDstSingleVaultStateReq calldata req_) external payable; + + /// @dev Performs multi destination x multi vault withdraws + /// @param req_ is the request object containing all the necessary data for the action + function multiDstMultiVaultWithdraw(MultiDstMultiVaultStateReq calldata req_) external payable; + + /// @dev Forwards dust to Paymaster + /// @param token_ the token to forward + function forwardDustToPaymaster(address token_) external; +} diff --git a/src/adapters/superform/IERC1155A.sol b/src/adapters/superform/IERC1155A.sol new file mode 100644 index 0000000..75451ba --- /dev/null +++ b/src/adapters/superform/IERC1155A.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "../../@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +/// @title IERC1155A +/// @author Zeropoint Labs +/// @dev Single/range based id approve capability with conversion to ERC20s +interface IERC1155A is IERC1155 { + ////////////////////////////////////////////////////////////// + // EVENTS // + ////////////////////////////////////////////////////////////// + + /// @dev emitted when single id approval is set + event ApprovalForOne(address indexed owner, address indexed spender, uint256 id, uint256 amount); + + /// @dev emitted when an ERC1155A id is transmuted to an aERC20 + event TransmutedToERC20(address indexed user, uint256 id, uint256 amount, address indexed receiver); + + /// @dev emitted when an aERC20 is transmuted to an ERC1155 id + event TransmutedToERC1155A(address indexed user, uint256 id, uint256 amount, address indexed receiver); + + /// @dev emitted when multiple ERC1155A ids are transmuted to aERC20s + event TransmutedBatchToERC20(address indexed user, uint256[] ids, uint256[] amounts, address indexed receiver); + + /// @dev emitted when multiple aERC20s are transmuted to ERC1155A ids + event TransmutedBatchToERC1155A(address indexed user, uint256[] ids, uint256[] amounts, address indexed receiver); + + ////////////////////////////////////////////////////////////// + // ERRORS // + ////////////////////////////////////////////////////////////// + + /// @dev thrown if aERC20 was already registered + error AERC20_ALREADY_REGISTERED(); + + /// @dev thrown if aERC20 was not registered + error AERC20_NOT_REGISTERED(); + + /// @dev thrown if allowance amount will be decreased below zero + error DECREASED_ALLOWANCE_BELOW_ZERO(); + + /// @dev thrown if the associated ERC1155A id has not been minted before registering an aERC20 + error ID_NOT_MINTED_YET(); + + /// @dev thrown if there is a length mismatch in batch operations + error LENGTH_MISMATCH(); + + /// @dev thrown if transfer is made to address 0 + error TRANSFER_TO_ADDRESS_ZERO(); + + /// @dev thrown if address is 0 + error ZERO_ADDRESS(); + + ////////////////////////////////////////////////////////////// + // EXTERNAL VIEW FUNCTIONS // + ////////////////////////////////////////////////////////////// + + /// @notice Public getter for existing single id total supply + /// @param id id of the ERC1155 + function totalSupply(uint256 id) external view returns (uint256); + + /// @notice Public getter to know if a token id exists + /// @dev determines based on total supply for the id + /// @param id id of the ERC1155 + function exists(uint256 id) external view returns (bool); + + /// @notice Public getter for existing single id approval + /// @param owner address of the owner of the ERC1155A id + /// @param spender address of the contract to approve + /// @param id id of the ERC1155A to approve + function allowance(address owner, address spender, uint256 id) external returns (uint256); + + /// @notice handy helper to check if a AERC20 is registered + /// @param id id of the ERC1155 + function aERC20Exists(uint256 id) external view returns (bool); + + /// @notice Public getter for the address of the aErc20 token for a given ERC1155 id + /// @param id id of the ERC1155 to get the aErc20 token address for + /// @return aERC20 address of the aErc20 token for the given ERC1155 id + function getERC20TokenAddress(uint256 id) external view returns (address aERC20); + + /// @notice Compute return string from baseURI set for this contract and unique vaultId + /// @param id id of the ERC1155 + function uri(uint256 id) external view returns (string memory); + + /// @notice ERC1155A name + function name() external view returns (string memory); + + /// @notice ERC1155A symbol + function symbol() external view returns (string memory); + + ////////////////////////////////////////////////////////////// + // EXTERNAL WRITE FUNCTIONS // + ////////////////////////////////////////////////////////////// + + /// @notice Public function for setting single id approval + /// @dev Notice `owner` param, it will always be msg.sender, see _setApprovalForOne() + /// @param spender address of the contract to approve + /// @param id id of the ERC1155A to approve + /// @param amount amount of the ERC1155A to approve + function setApprovalForOne(address spender, uint256 id, uint256 amount) external; + + /// @notice Public function for setting multiple id approval + /// @dev extension of sigle id approval + /// @param spender address of the contract to approve + /// @param ids ids of the ERC1155A to approve + /// @param amounts amounts of the ERC1155A to approve + function setApprovalForMany(address spender, uint256[] memory ids, uint256[] memory amounts) external; + + /// @notice Public function for increasing single id approval amount + /// @dev Re-adapted from ERC20 + /// @param spender address of the contract to approve + /// @param id id of the ERC1155A to approve + /// @param addedValue amount of the allowance to increase by + function increaseAllowance(address spender, uint256 id, uint256 addedValue) external returns (bool); + + /// @notice Public function for decreasing single id approval amount + /// @dev Re-adapted from ERC20 + /// @param spender address of the contract to approve + /// @param id id of the ERC1155A to approve + /// @param subtractedValue amount of the allowance to decrease by + function decreaseAllowance(address spender, uint256 id, uint256 subtractedValue) external returns (bool); + + /// @notice Public function for increasing multiple id approval amount at once + /// @dev extension of single id increase allowance + /// @param spender address of the contract to approve + /// @param ids ids of the ERC1155A to approve + /// @param addedValues amounts of the allowance to increase by + function increaseAllowanceForMany(address spender, uint256[] memory ids, uint256[] memory addedValues) + external + returns (bool); + + /// @notice Public function for decreasing multiple id approval amount at once + /// @dev extension of single id decrease allowance + /// @param spender address of the contract to approve + /// @param ids ids of the ERC1155A to approve + /// @param subtractedValues amounts of the allowance to decrease by + function decreaseAllowanceForMany(address spender, uint256[] memory ids, uint256[] memory subtractedValues) + external + returns (bool); + + /// @notice Turn ERC1155A id into an aERC20 + /// @dev allows owner to send ERC1155A id as an aERC20 to receiver + /// @param owner address of the user on whose behalf this transmutation is happening + /// @param id id of the ERC20s to transmute to aERC20 + /// @param amount amount of the ERC20s to transmute to aERC20 + /// @param receiver address of the user to receive the aERC20 token + function transmuteToERC20(address owner, uint256 id, uint256 amount, address receiver) external; + + /// @notice Turn aERC20 into an ERC1155A id + /// @dev allows owner to send ERC20 as an ERC1155A id to receiver + /// @param owner address of the user on whose behalf this transmutation is happening + /// @param id id of the ERC20s to transmute to erc1155 + /// @param amount amount of the ERC20s to transmute to erc1155 + /// @param receiver address of the user to receive the erc1155 token id + function transmuteToERC1155A(address owner, uint256 id, uint256 amount, address receiver) external; + + /// @notice Turn ERC1155A ids into aERC20s + /// @dev allows owner to send ERC1155A ids as aERC20s to receiver + /// @param owner address of the user on whose behalf this transmutation is happening + /// @param ids ids of the ERC1155A to transmute + /// @param amounts amounts of the ERC1155A to transmute + /// @param receiver address of the user to receive the aERC20 tokens + function transmuteBatchToERC20(address owner, uint256[] memory ids, uint256[] memory amounts, address receiver) + external; + + /// @notice Turn aERC20s into ERC1155A ids + /// @dev allows owner to send aERC20s as ERC1155A ids to receiver + /// @param owner address of the user on whose behalf this transmutation is happening + /// @param ids ids of the ERC20 to transmute + /// @param amounts amounts of the ERC20 to transmute + /// @param receiver address of the user to receive the ERC1155 token ids + function transmuteBatchToERC1155A(address owner, uint256[] memory ids, uint256[] memory amounts, address receiver) + external; + + /// @notice payable to allow any implementing cross-chain protocol to be paid for fees for broadcasting + /// @dev should emit any required events inside _registerAERC20 internal function + /// @param id of the ERC1155 to create a ERC20 for + function registerAERC20(uint256 id) external payable returns (address); +} diff --git a/src/adapters/superform/IRewardsDistributor.sol b/src/adapters/superform/IRewardsDistributor.sol new file mode 100644 index 0000000..b62da8c --- /dev/null +++ b/src/adapters/superform/IRewardsDistributor.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.27; + +/// @title IRewardsDistributor +/// @notice interface for the RewardsDistributor contract +/// @author Zeropoint Labs +interface IRewardsDistributor { + ////////////////////////////////////////////////////////////// + // ERRORS // + ////////////////////////////////////////////////////////////// + + /// @dev error message for not being a rewards admin + error NOT_REWARDS_ADMIN(); + + /// @dev error message for an invalid claim + error INVALID_CLAIM(); + + /// @dev error message for an invalid batch request (different array lengths) + error INVALID_BATCH_REQ(); + + /// @dev error message for an invalid claim (invalid token/amounts array length) + error INVALID_REQ_TOKENS_AMOUNTS(); + + /// @dev error message for an invalid batch claim (invalid token/amounts array length) + error INVALID_BATCH_REQ_TOKENS_AMOUNTS(); + + /// @dev error message for an invalid merkle root + error INVALID_MERKLE_ROOT(); + + /// @dev error message for an invalid receiver + error INVALID_RECEIVER(); + + /// @dev error message for when merkle root is not set + error MERKLE_ROOT_NOT_SET(); + + /// @dev error message for when the array length is zero + error ZERO_ARR_LENGTH(); + + /// @dev error message for when the claimer has already claimed their rewards + error ALREADY_CLAIMED(); + + /// @dev error message for when the claim deadline has passed + error CLAIM_DEADLINE_PASSED(); + + ////////////////////////////////////////////////////////////// + // EVENTS // + ////////////////////////////////////////////////////////////// + + /// @dev Emitted when tokens are claimed. + event RewardsClaimed( + address indexed claimer, + address indexed receiver, + uint256 periodId, + address[] rewardTokens_, + uint256[] amountsClaimed_ + ); + + /// @dev Emitted when new periodic rewards are set. + event PeriodicRewardsSet(uint256 indexed periodId, bytes32 merkleRoot, uint256 startTimestamp); + + ////////////////////////////////////////////////////////////// + // STRUCTS // + ////////////////////////////////////////////////////////////// + + struct PeriodicRewardsData { + uint256 startTimestamp; + bytes32 merkleRoot; + } + + ////////////////////////////////////////////////////////////// + // EXTERNAL WRITE FUNCTIONS // + ////////////////////////////////////////////////////////////// + + /// @notice allows owner to set the merkle root for the current period rewards + /// @param root_ is the merkle root for that period generated offchain + /// @dev [gas-opt]: function is payable to avoid msg.value checks + function setPeriodicRewards(bytes32 root_) external payable; + + /// @notice lets an account claim a given quantity of reward tokens. + /// @param receiver_ is the receiver of the tokens to claim. + /// @param periodId_ is the specific period to claim + /// @param rewardTokens_ are the address of the rewards token to claim on the specific period + /// @param amountsClaimed_ adre the amount of tokens to claim for each reward token + /// @param proof_ the merkle proof + function claim( + address receiver_, + uint256 periodId_, + address[] calldata rewardTokens_, + uint256[] calldata amountsClaimed_, + bytes32[] calldata proof_ + ) external; + + /// @notice is a batching version of claim() + function batchClaim( + address receiver_, + uint256[] calldata periodIds_, + address[][] calldata rewardTokens_, + uint256[][] calldata amountsClaimed_, + bytes32[][] calldata proofs_ + ) external; + + /// @notice allows the owner to rescue any ERC20 tokens sent to the contract + /// @param rewardTokens_ are the address of the rewards token to claim on the specific period + /// @param amounts_ are the amount of tokens to claim for each reward token + function rescueRewards(address[] calldata rewardTokens_, uint256[] calldata amounts_) external; + + /// @notice allows the owner to invalidate a period + /// @param periodId_ is the period identifier + function invalidatePeriod(uint256 periodId_) external; + + ////////////////////////////////////////////////////////////// + // EXTERNAL VIEW FUNCTIONS // + ////////////////////////////////////////////////////////////// + + /// @notice helps validate if the claim is valid + /// @param claimer_ is the address of the claiming wallet + /// @param periodId_ is the period identifier + /// @param rewardTokens_ are the address of the rewards token to claim on the specific period + /// @param amountsClaimed_ adre the amount of tokens to claim for each reward token + /// @param proof_ is the merkle proof + /// @dev returns false even if proof is valid and user already claimed their periodic rewards + function verifyClaim( + address claimer_, + uint256 periodId_, + address[] calldata rewardTokens_, + uint256[] calldata amountsClaimed_, + bytes32[] calldata proof_ + ) external view returns (bool valid); + + function currentPeriodId() external returns (uint256); +} diff --git a/src/adapters/superform/p2pSuperformProxy/IP2pSuperformProxy.sol b/src/adapters/superform/p2pSuperformProxy/IP2pSuperformProxy.sol new file mode 100644 index 0000000..46d025b --- /dev/null +++ b/src/adapters/superform/p2pSuperformProxy/IP2pSuperformProxy.sol @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../../../@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import "../../../p2pYieldProxy/IP2pYieldProxy.sol"; + +interface IP2pSuperformProxy is IP2pYieldProxy, IERC1155Receiver { + event P2pSuperformProxy__Claimed( + address indexed _token, uint256 _totalAmount, uint256 _p2pAmount, uint256 _clientAmount + ); + + /// @notice Withdraw assets from Superform protocol + /// @param _superformCalldata calldata for withdraw function of Superform protocol + function withdraw(bytes calldata _superformCalldata) external; + + /// @notice Withdraw only accrued rewards from Superform protocol + /// @param _superformCalldata calldata for withdraw function of Superform protocol + function withdrawAccruedRewards(bytes calldata _superformCalldata) external; + + /// @notice Claims queued rewards from the Superform rewards distributor + /// @param _periodIds Reward period identifiers being claimed + /// @param _rewardTokens Nested array containing reward token addresses for each period + /// @param _amountsClaimed Nested array containing amounts being claimed for each reward token + /// @param _proofs Nested array with Merkle proofs proving eligibility for each reward entry + function batchClaim( + uint256[] calldata _periodIds, + address[][] calldata _rewardTokens, + uint256[][] calldata _amountsClaimed, + bytes32[][] calldata _proofs + ) external; +} diff --git a/src/adapters/superform/p2pSuperformProxy/P2pSuperformProxy.sol b/src/adapters/superform/p2pSuperformProxy/P2pSuperformProxy.sol new file mode 100644 index 0000000..c83c25d --- /dev/null +++ b/src/adapters/superform/p2pSuperformProxy/P2pSuperformProxy.sol @@ -0,0 +1,309 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../../../p2pYieldProxy/P2pYieldProxy.sol"; +import "../IBaseForm.sol"; +import "../IBaseRouter.sol"; +import "../IERC1155A.sol"; +import "../IRewardsDistributor.sol"; +import "../p2pSuperformProxyFactory/IP2pSuperformProxyFactory.sol"; +import "./IP2pSuperformProxy.sol"; + +error P2pSuperformProxy__SuperformCalldataTooShort(); +error P2pSuperformProxy__SelectorNotSupported(bytes4 _selector); +error P2pSuperformProxy__NativeAmountToDepositAfterFeeLessThanliqRequestNativeAmount( + uint256 _nativeAmountToDepositAfterFee, uint256 _liqRequestNativeAmount +); +error P2pSuperformProxy__ShouldNotRetain4626(); +error P2pSuperformProxy__ReceiverAddressShouldBeP2pSuperformProxy(address _receiverAddress); +error P2pSuperformProxy__ReceiverAddressSPShouldBeP2pSuperformProxy(address _receiverAddressSP); +error P2pSuperformProxy__AssetShouldNotBeZeroAddress(); +error P2pSuperformProxy__NotClaimed(address _token); +error P2pSuperformProxy__NoAccruedRewards(uint256 _vaultId, address _asset); +error P2pSuperformProxy__WithdrawAmountExceedsAccrued(uint256 _requestedAssets, uint256 _availableRewards); + +contract P2pSuperformProxy is P2pYieldProxy, IP2pSuperformProxy { + using SafeERC20 for IERC20; + + address internal immutable i_superPositions; + IRewardsDistributor internal immutable i_rewardsDistributor; + + /// @notice Constructor for P2pEthenaProxy + /// @param _factory Factory address + /// @param _p2pTreasury P2pTreasury address + /// @param _superformRouter SuperformRouter address + /// @param _superPositions SuperPositions address + /// @param _allowedCalldataChecker AllowedCalldataChecker + /// @param _rewardsDistributor RewardsDistributor + constructor( + address _factory, + address _p2pTreasury, + address _superformRouter, + address _superPositions, + address _allowedCalldataChecker, + address _rewardsDistributor + ) P2pYieldProxy(_factory, _p2pTreasury, _superformRouter, _allowedCalldataChecker) { + i_superPositions = _superPositions; + i_rewardsDistributor = IRewardsDistributor(_rewardsDistributor); + } + + /// @notice Accept ether from transactions + receive() external payable {} + + /// @inheritdoc IP2pYieldProxy + function deposit(bytes calldata _yieldProtocolDepositCalldata) + external + payable + override(P2pYieldProxy, IP2pYieldProxy) + { + require(_yieldProtocolDepositCalldata.length > 4, P2pSuperformProxy__SuperformCalldataTooShort()); + + bytes4 selector = bytes4(_yieldProtocolDepositCalldata[:4]); + require( + selector == IBaseRouter.singleDirectSingleVaultDeposit.selector, + P2pSuperformProxy__SelectorNotSupported(selector) + ); + + SingleDirectSingleVaultStateReq memory req = + abi.decode(_yieldProtocolDepositCalldata[4:], (SingleDirectSingleVaultStateReq)); + + uint256 nativeAmountToDepositAfterFee = msg.value * s_clientBasisPointsOfDeposit / 10_000; + + address asset; + if (req.superformData.liqRequest.token == address(0)) { + address superform = address(uint160(req.superformData.superformId)); + IERC4626 vault = IERC4626(superform); + asset = vault.asset(); + } else { + asset = req.superformData.liqRequest.token; + } + + bool isNative = asset == NATIVE; + if (isNative) { + require( + nativeAmountToDepositAfterFee >= req.superformData.liqRequest.nativeAmount, + P2pSuperformProxy__NativeAmountToDepositAfterFeeLessThanliqRequestNativeAmount( + nativeAmountToDepositAfterFee, req.superformData.liqRequest.nativeAmount + ) + ); + } else { + require(asset != address(0), P2pSuperformProxy__AssetShouldNotBeZeroAddress()); + // ETH can still be used to pay for bridging, swaps, etc., so msg.value can be > 0 + } + require(!req.superformData.retain4626, P2pSuperformProxy__ShouldNotRetain4626()); + require( + req.superformData.receiverAddress == address(this), + P2pSuperformProxy__ReceiverAddressShouldBeP2pSuperformProxy(req.superformData.receiverAddress) + ); + require( + req.superformData.receiverAddressSP == address(this), + P2pSuperformProxy__ReceiverAddressSPShouldBeP2pSuperformProxy(req.superformData.receiverAddressSP) + ); + + uint256 amount = isNative ? 0 : calculateMinAmountToApproveForDeposit(req.superformData.amount); + + _deposit(req.superformData.superformId, asset, amount, _yieldProtocolDepositCalldata, isNative); + } + + /// @inheritdoc IP2pSuperformProxy + function withdraw(bytes calldata _superformCalldata) external onlyClient { + require(_superformCalldata.length > 4, P2pSuperformProxy__SuperformCalldataTooShort()); + bytes4 selector = bytes4(_superformCalldata[:4]); + + require( + selector == IBaseRouter.singleDirectSingleVaultWithdraw.selector, + P2pSuperformProxy__SelectorNotSupported(selector) + ); + + SingleDirectSingleVaultStateReq memory req = + abi.decode(_superformCalldata[4:], (SingleDirectSingleVaultStateReq)); + + require( + req.superformData.receiverAddress == address(this), + P2pSuperformProxy__ReceiverAddressShouldBeP2pSuperformProxy(req.superformData.receiverAddress) + ); + require( + req.superformData.receiverAddressSP == address(this), + P2pSuperformProxy__ReceiverAddressSPShouldBeP2pSuperformProxy(req.superformData.receiverAddressSP) + ); + + address asset; + if (req.superformData.liqRequest.token == address(0)) { + address superform = address(uint160(req.superformData.superformId)); + IERC4626 vault = IERC4626(superform); + asset = vault.asset(); + } else { + asset = req.superformData.liqRequest.token; + } + require(asset != address(0), P2pSuperformProxy__AssetShouldNotBeZeroAddress()); + + IERC1155A(i_superPositions).increaseAllowance( + i_yieldProtocolAddress, req.superformData.superformId, req.superformData.amount + ); + + _withdraw(req.superformData.superformId, asset, _superformCalldata); + } + + /// @inheritdoc IP2pSuperformProxy + function withdrawAccruedRewards(bytes calldata _superformCalldata) external onlyP2pOperator { + require(_superformCalldata.length > 4, P2pSuperformProxy__SuperformCalldataTooShort()); + bytes4 selector = bytes4(_superformCalldata[:4]); + + require( + selector == IBaseRouter.singleDirectSingleVaultWithdraw.selector, + P2pSuperformProxy__SelectorNotSupported(selector) + ); + + SingleDirectSingleVaultStateReq memory req = + abi.decode(_superformCalldata[4:], (SingleDirectSingleVaultStateReq)); + + require( + req.superformData.receiverAddress == address(this), + P2pSuperformProxy__ReceiverAddressShouldBeP2pSuperformProxy(req.superformData.receiverAddress) + ); + require( + req.superformData.receiverAddressSP == address(this), + P2pSuperformProxy__ReceiverAddressSPShouldBeP2pSuperformProxy(req.superformData.receiverAddressSP) + ); + + address asset; + if (req.superformData.liqRequest.token == address(0)) { + address superform = address(uint160(req.superformData.superformId)); + IERC4626 vault = IERC4626(superform); + asset = vault.asset(); + } else { + asset = req.superformData.liqRequest.token; + } + require(asset != address(0), P2pSuperformProxy__AssetShouldNotBeZeroAddress()); + + int256 accruedRewards = calculateAccruedRewards(req.superformData.superformId, asset); + if (accruedRewards <= 0) { + revert P2pSuperformProxy__NoAccruedRewards(req.superformData.superformId, asset); + } + + uint256 accruedRewardsPositive = uint256(accruedRewards); + + uint256 requestedAssets = + IBaseForm(address(uint160(req.superformData.superformId))).previewRedeemFrom(req.superformData.amount); + if (requestedAssets > accruedRewardsPositive) { + revert P2pSuperformProxy__WithdrawAmountExceedsAccrued(requestedAssets, accruedRewardsPositive); + } + + IERC1155A(i_superPositions).increaseAllowance( + i_yieldProtocolAddress, req.superformData.superformId, req.superformData.amount + ); + + _withdraw(req.superformData.superformId, asset, _superformCalldata); + } + + /// @inheritdoc IP2pSuperformProxy + function batchClaim( + uint256[] calldata _periodIds, + address[][] calldata _rewardTokens, + uint256[][] calldata _amountsClaimed, + bytes32[][] calldata _proofs + ) external nonReentrant { + if (msg.sender != s_client) { + IP2pSuperformProxyFactory(address(i_factory)).checkClaim(msg.sender); + } + + // Determine the worst-case total number of token addresses. + uint256 totalTokens = 0; + for (uint256 i = 0; i < _rewardTokens.length; i++) { + totalTokens += _rewardTokens[i].length; + } + + // Allocate a memory array for potential unique tokens. + address[] memory uniqueTokens = new address[](totalTokens); + uint256 uniqueCount = 0; + + // Loop through each subarray and each token. + // For every token, perform a linear search on the uniqueTokens array. + // If the token is not already present, add it. + for (uint256 i = 0; i < _rewardTokens.length; i++) { + address[] calldata tokenGroup = _rewardTokens[i]; + for (uint256 j = 0; j < tokenGroup.length; j++) { + address token = tokenGroup[j]; + bool found = false; + for (uint256 k = 0; k < uniqueCount; k++) { + if (uniqueTokens[k] == token) { + found = true; + break; + } + } + if (!found) { + uniqueTokens[uniqueCount] = token; + uniqueCount++; + } + } + } + + uint256[] memory assetAmountsBefore = new uint256[](uniqueCount); + for (uint256 i = 0; i < uniqueCount; i++) { + address token = uniqueTokens[i]; + assetAmountsBefore[i] = IERC20(token).balanceOf(address(this)); + } + + // claim _reward token from Superform + i_rewardsDistributor.batchClaim(address(this), _periodIds, _rewardTokens, _amountsClaimed, _proofs); + + for (uint256 i = 0; i < uniqueCount; i++) { + address token = uniqueTokens[i]; + uint256 assetAmountAfter = IERC20(token).balanceOf(address(this)); + + uint256 newAssetAmount = assetAmountAfter - assetAmountsBefore[i]; + require(newAssetAmount > 0, P2pSuperformProxy__NotClaimed(token)); + + uint256 p2pAmount = calculateP2pFeeAmount(newAssetAmount); + uint256 clientAmount = newAssetAmount - p2pAmount; + + if (p2pAmount > 0) { + IERC20(token).safeTransfer(i_p2pTreasury, p2pAmount); + } + // clientAmount must be > 0 at this point + IERC20(token).safeTransfer(s_client, clientAmount); + + emit P2pSuperformProxy__Claimed(token, newAssetAmount, p2pAmount, clientAmount); + } + } + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) { + return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")); + } + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + returns (bytes4) + { + return bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")); + } + + /// @inheritdoc IP2pYieldProxy + function calculateAccruedRewards(uint256 _vaultId, address _asset) + public + view + override(IP2pYieldProxy, P2pYieldProxy) + returns (int256) + { + uint256 shares = IERC1155A(i_superPositions).balanceOf(address(this), _vaultId); + IBaseForm vault = IBaseForm(address(uint160(_vaultId))); + uint256 currentAmount = vault.previewRedeemFrom(shares); + uint256 userPrincipal = getUserPrincipal(_vaultId, _asset); + return int256(currentAmount) - int256(userPrincipal); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(P2pYieldProxy, IERC165) + returns (bool) + { + return interfaceId == type(IP2pSuperformProxy).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId + || super.supportsInterface(interfaceId); + } +} diff --git a/src/adapters/superform/p2pSuperformProxyFactory/IP2pSuperformProxyFactory.sol b/src/adapters/superform/p2pSuperformProxyFactory/IP2pSuperformProxyFactory.sol new file mode 100644 index 0000000..ddb0e44 --- /dev/null +++ b/src/adapters/superform/p2pSuperformProxyFactory/IP2pSuperformProxyFactory.sol @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../../../p2pYieldProxyFactory/IP2pYieldProxyFactory.sol"; + +/// @dev External interface of P2pSuperformProxyFactory +interface IP2pSuperformProxyFactory is IP2pYieldProxyFactory { + /// @notice Validates that an address is the authorised P2pOperator for reward claims + /// @param _p2pOperatorToCheck Address being validated as the active P2pOperator + function checkClaim(address _p2pOperatorToCheck) external view; +} diff --git a/src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol b/src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol new file mode 100644 index 0000000..544d2d3 --- /dev/null +++ b/src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../../../p2pYieldProxyFactory/P2pYieldProxyFactory.sol"; +import "../p2pSuperformProxy/P2pSuperformProxy.sol"; +import "./IP2pSuperformProxyFactory.sol"; +import {IERC4626} from "../../../@openzeppelin/contracts/interfaces/IERC4626.sol"; + +/// @title Entry point for depositing into Superform with P2P.org +contract P2pSuperformProxyFactory is P2pYieldProxyFactory, IP2pSuperformProxyFactory { + /// @notice Constructor for P2pSuperformProxyFactory + /// @param _p2pSigner The P2pSigner address + /// @param _p2pOperator The P2pOperator address + /// @param _p2pTreasury The P2pTreasury address + /// @param _superformRouter SuperformRouter address + /// @param _superPositions SuperPositions address + /// @param _allowedCalldataChecker AllowedCalldataChecker + /// @param _rewardsDistributor RewardsDistributor + constructor( + address _p2pSigner, + address _p2pOperator, + address _p2pTreasury, + address _superformRouter, + address _superPositions, + address _allowedCalldataChecker, + address _rewardsDistributor + ) P2pYieldProxyFactory(_p2pSigner, _p2pOperator) { + i_referenceP2pYieldProxy = new P2pSuperformProxy( + address(this), _p2pTreasury, _superformRouter, _superPositions, _allowedCalldataChecker, _rewardsDistributor + ); + } + + /// @dev Checks if the claim is valid + /// @param _p2pOperatorToCheck The P2pOperator to check + function checkClaim(address _p2pOperatorToCheck) public view { + require(getP2pOperator() == _p2pOperatorToCheck, P2pOperator__UnauthorizedAccount(_p2pOperatorToCheck)); + } + + /// @inheritdoc IP2pYieldProxyFactory + function transferP2pOperator(address _newP2pOperator) + public + override(P2pYieldProxyFactory, IP2pYieldProxyFactory) + { + P2pYieldProxyFactory.transferP2pOperator(_newP2pOperator); + } + + /// @inheritdoc IP2pYieldProxyFactory + function acceptP2pOperator() public override(P2pYieldProxyFactory, IP2pYieldProxyFactory) { + P2pYieldProxyFactory.acceptP2pOperator(); + } + + /// @inheritdoc IP2pYieldProxyFactory + function getPendingP2pOperator() + public + view + override(P2pYieldProxyFactory, IP2pYieldProxyFactory) + returns (address) + { + return P2pYieldProxyFactory.getPendingP2pOperator(); + } + + /// @inheritdoc ERC165 + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(P2pYieldProxyFactory, IERC165) + returns (bool) + { + return interfaceId == type(IP2pSuperformProxyFactory).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IP2pYieldProxyFactory + function getP2pOperator() public view override(P2pYieldProxyFactory, IP2pYieldProxyFactory) returns (address) { + return P2pYieldProxyFactory.getP2pOperator(); + } +} diff --git a/src/common/AllowedCalldataChecker.sol b/src/common/AllowedCalldataChecker.sol index 3e48de4..e658edb 100644 --- a/src/common/AllowedCalldataChecker.sol +++ b/src/common/AllowedCalldataChecker.sol @@ -3,51 +3,22 @@ pragma solidity 0.8.27; +import "../@openzeppelin/contracts-upgradable/proxy/utils/Initializable.sol"; import "./IAllowedCalldataChecker.sol"; -import "./P2pStructs.sol"; -/// @dev Error for when the calldata is too short -error AllowedCalldataChecker__DataTooShort(); +/// @dev No extra calls are allowed for now. AllowedCalldataChecker can be upgraded in the future. +error AllowedCalldataChecker__NoAllowedCalldata(); /// @title AllowedCalldataChecker /// @author P2P Validator -/// @notice Abstract contract for checking if a calldata is allowed -abstract contract AllowedCalldataChecker is IAllowedCalldataChecker { - - /// @dev Modifier for checking if a calldata is allowed - /// @param _yieldProtocolAddress The address of the yield protocol - /// @param _yieldProtocolCalldata The calldata (encoded signature + arguments) to be passed to the yield protocol - modifier calldataShouldBeAllowed( - address _yieldProtocolAddress, - bytes calldata _yieldProtocolCalldata - ) { - // validate yieldProtocolCalldata for yieldProtocolAddress - bytes4 selector = _getFunctionSelector(_yieldProtocolCalldata); - checkCalldata( - _yieldProtocolAddress, - selector, - _yieldProtocolCalldata[4:] - ); - _; +/// @notice Upgradable contract for checking if a calldata is allowed +contract AllowedCalldataChecker is IAllowedCalldataChecker, Initializable { + function initialize() public initializer { + // do nothing in this implementation } - /// @notice Returns function selector (first 4 bytes of data) - /// @param _data calldata (encoded signature + arguments) - /// @return functionSelector function selector - function _getFunctionSelector( - bytes calldata _data - ) private pure returns (bytes4 functionSelector) { - require (_data.length >= 4, AllowedCalldataChecker__DataTooShort()); - return bytes4(_data[:4]); + /// @inheritdoc IAllowedCalldataChecker + function checkCalldata(address, bytes4, bytes calldata) public pure { + revert AllowedCalldataChecker__NoAllowedCalldata(); } - - /// @notice Checks if the calldata is allowed - /// @param _target The address of the yield protocol - /// @param _selector The selector of the function - /// @param _calldataAfterSelector The calldata after the selector - function checkCalldata( - address _target, - bytes4 _selector, - bytes calldata _calldataAfterSelector - ) public virtual view; } diff --git a/src/common/IAllowedCalldataChecker.sol b/src/common/IAllowedCalldataChecker.sol index 4adc8ce..57deaa5 100644 --- a/src/common/IAllowedCalldataChecker.sol +++ b/src/common/IAllowedCalldataChecker.sol @@ -3,15 +3,13 @@ pragma solidity 0.8.27; -import "./P2pStructs.sol"; - /// @title IAllowedCalldataChecker /// @author P2P Validator /// @notice Interface for checking if a calldata is allowed interface IAllowedCalldataChecker { - function checkCalldata( - address _target, - bytes4 _selector, - bytes calldata _calldataAfterSelector - ) external view; + /// @notice Checks if the calldata is allowed + /// @param _target The address of the yield protocol + /// @param _selector The selector of the function + /// @param _calldataAfterSelector The calldata after the selector + function checkCalldata(address _target, bytes4 _selector, bytes calldata _calldataAfterSelector) external view; } diff --git a/src/common/P2pStructs.sol b/src/common/P2pStructs.sol deleted file mode 100644 index 2a7ff68..0000000 --- a/src/common/P2pStructs.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2025 P2P Validator -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; - -abstract contract P2pStructs { - /// @title Enum representing the type of rule for allowed calldata - enum RuleType { - /// @notice No calldata beyond selector is allowed - None, - /// @notice Any calldata beyond selector is allowed - AnyCalldata, - /// @notice Limits calldata starting from index to match allowedBytes - StartsWith, - /// @notice Limits calldata ending at index to match allowedBytes - EndsWith - } - - /// @notice Struct representing a rule for allowed calldata - /// @param ruleType The type of rule - /// @param index The start (or end, depending on StartsWith/EndsWith) index of the bytes to check - /// @param allowedBytes The allowed bytes - struct Rule { - RuleType ruleType; - uint32 index; - bytes allowedBytes; - } -} diff --git a/src/p2pYieldProxy/IP2pYieldProxy.sol b/src/p2pYieldProxy/IP2pYieldProxy.sol index daca1a8..38cc288 100644 --- a/src/p2pYieldProxy/IP2pYieldProxy.sol +++ b/src/p2pYieldProxy/IP2pYieldProxy.sol @@ -4,66 +4,63 @@ pragma solidity 0.8.27; import "../@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import "../@permit2/interfaces/IAllowanceTransfer.sol"; -import "../common/IAllowedCalldataChecker.sol"; /// @dev External interface of P2pYieldProxy declared to support ERC165 detection. -interface IP2pYieldProxy is IAllowedCalldataChecker, IERC165 { - +interface IP2pYieldProxy is IERC165 { /// @notice Emitted when the P2pYieldProxy is initialized event P2pYieldProxy__Initialized(); /// @notice Emitted when a deposit is made event P2pYieldProxy__Deposited( - address indexed _yieldProtocolAddress, - address indexed _asset, - uint256 _amount, - uint256 _totalDepositedAfter + uint256 indexed _vaultId, address indexed _asset, uint256 _amountAfterFee, uint256 _totalDepositedAfter ); + event P2pYieldProxy__DepositFee(address indexed _asset, uint256 _amount); + /// @notice Emitted when a withdrawal is made event P2pYieldProxy__Withdrawn( - address indexed _yieldProtocolAddress, - address indexed _vault, + uint256 indexed _vaultId, address indexed _asset, uint256 _assets, uint256 _totalWithdrawnAfter, - uint256 _newProfit, + int256 _accruedRewards, uint256 _p2pAmount, uint256 _clientAmount ); + /// @notice Emergency withdrawal queue flow + event P2pYieldProxy__EmergencyWithdrawalQueueFlow(uint256 indexed _vaultId, address indexed _asset); + + /// @notice Direct asset recovery from P2pYieldProxy + event P2pYieldProxy__EmergencyWithdrawn(address indexed _asset, uint256 _amount); + /// @notice Emitted when an arbitrary allowed function is called - event P2pYieldProxy__CalledAsAnyFunction( - address indexed _yieldProtocolAddress - ); + event P2pYieldProxy__CalledAsAnyFunction(address indexed _yieldProtocolAddress); /// @notice Initializes the P2pYieldProxy /// @param _client The client address - /// @param _clientBasisPoints The client basis points - function initialize( - address _client, - uint96 _clientBasisPoints - ) - external; - - /// @notice Deposits assets into the yield protocol - /// @param _permitSingleForP2pYieldProxy The permit single for the P2pYieldProxy - /// @param _permit2SignatureForP2pYieldProxy The permit2 signature for the P2pYieldProxy - function deposit( - IAllowanceTransfer.PermitSingle calldata _permitSingleForP2pYieldProxy, - bytes calldata _permit2SignatureForP2pYieldProxy - ) - external; + /// @param _clientBasisPointsOfDeposit The client basis points (share) of deposit + /// @param _clientBasisPointsOfProfit The client basis points (share) of profit + function initialize(address _client, uint48 _clientBasisPointsOfDeposit, uint48 _clientBasisPointsOfProfit) + external; + + /// @notice Deposits assets into a specific vault handled by the proxy. + /// @param _yieldProtocolDepositCalldata Calldata that performs the actual deposit on the yield protocol. + function deposit(bytes calldata _yieldProtocolDepositCalldata) external payable; /// @notice Calls an arbitrary allowed function /// @param _yieldProtocolAddress The address of the yield protocol /// @param _yieldProtocolCalldata The calldata to call the yield protocol - function callAnyFunction( - address _yieldProtocolAddress, - bytes calldata _yieldProtocolCalldata - ) - external; + function callAnyFunction(address _yieldProtocolAddress, bytes calldata _yieldProtocolCalldata) external; + + /// @notice Withdraw all ERC20 from P2pYieldProxy balance + /// @dev Only callable by client in case of emergency + /// @param _token ERC20 token + function emergencyTokenWithdraw(address _token) external; + + /// @notice Withdraw all ETH from P2pYieldProxy balance + /// @dev Only callable by client in case of emergency + function emergencyNativeWithdraw() external; /// @notice Gets the factory address /// @return The factory address @@ -73,21 +70,61 @@ interface IP2pYieldProxy is IAllowedCalldataChecker, IERC165 { /// @return The P2pTreasury address function getP2pTreasury() external view returns (address); + /// @notice Gets the Yield Protocol address + /// @return The Yield Protocol address + function getYieldProtocolAddress() external view returns (address); + + /// @notice Gets the AllowedCalldataChecker address + /// @return The AllowedCalldataChecker address + function getAllowedCalldataChecker() external view returns (address); + /// @notice Gets the client address /// @return The client address function getClient() external view returns (address); - /// @notice Gets the client basis points - /// @return The client basis points - function getClientBasisPoints() external view returns (uint96); + /// @notice Gets the client basis points of deposit + /// @return The client basis points of deposit + function getClientBasisPointsOfDeposit() external view returns (uint48); + + /// @notice Gets the client basis points of profit + /// @return The client basis points of profit + function getClientBasisPointsOfProfit() external view returns (uint48); /// @notice Gets the total deposited for an asset + /// @param _vaultId vault ID /// @param _asset The asset address /// @return The total deposited - function getTotalDeposited(address _asset) external view returns (uint256); + function getTotalDeposited(uint256 _vaultId, address _asset) external view returns (uint256); /// @notice Gets the total withdrawn for an asset + /// @param _vaultId vault ID + /// @param _asset The asset address + /// @return The total withdrawn amount + function getTotalWithdrawn(uint256 _vaultId, address _asset) external view returns (uint256); + + /// @notice Calculates the outstanding user principal for a vault and asset + /// @param _vaultId vault ID + /// @param _asset The asset address + /// @return The remaining principal balance for the user + function getUserPrincipal(uint256 _vaultId, address _asset) external view returns (uint256); + + /// @notice Calculates the net rewards accrued for a vault and asset + /// @param _vaultId vault ID + /// @param _asset The asset address + /// @return The accrued rewards as a signed integer (negative values indicate a loss) + function calculateAccruedRewards(uint256 _vaultId, address _asset) external view returns (int256); + + /// @notice Returns the timestamp of the last fee collection for a vault and asset + /// @param _vaultId vault ID /// @param _asset The asset address - /// @return The total withdrawn - function getTotalWithdrawn(address _asset) external view returns (uint256); + /// @return lastFeeCollectionTime Timestamp of the most recent fee collection + function getLastFeeCollectionTime(uint256 _vaultId, address _asset) + external + view + returns (uint48 lastFeeCollectionTime); + + /// @notice Calculates the minimum amount of tokens that must be approved for a deposit + /// @param _amountToDeposit The desired amount of tokens to be deposited into the yield protocol + /// @return The token amount that needs to be approved for transfer to cover the deposit and fee + function calculateMinAmountToApproveForDeposit(uint256 _amountToDeposit) external view returns (uint256); } diff --git a/src/p2pYieldProxy/P2pYieldProxy.sol b/src/p2pYieldProxy/P2pYieldProxy.sol index 98f480c..1a06910 100644 --- a/src/p2pYieldProxy/P2pYieldProxy.sol +++ b/src/p2pYieldProxy/P2pYieldProxy.sol @@ -3,87 +3,62 @@ pragma solidity 0.8.27; -import "../@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "../@openzeppelin/contracts-upgradable/security/ReentrancyGuardUpgradeable.sol"; import "../@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../@openzeppelin/contracts/utils/Address.sol"; import "../@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "../@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import "../@permit2/interfaces/IAllowanceTransfer.sol"; -import "../@permit2/libraries/Permit2Lib.sol"; -import "../common/AllowedCalldataChecker.sol"; -import "../common/P2pStructs.sol"; +import "../common/IAllowedCalldataChecker.sol"; import "../p2pYieldProxyFactory/IP2pYieldProxyFactory.sol"; +import "../structs/P2pStructs.sol"; import "./IP2pYieldProxy.sol"; import {IERC4626} from "../@openzeppelin/contracts/interfaces/IERC4626.sol"; -/// @dev Error when the asset address is zero error P2pYieldProxy__ZeroAddressAsset(); - -/// @dev Error when the asset amount is zero -error P2pYieldProxy__ZeroAssetAmount(); - -/// @dev Error when the shares amount is zero -error P2pYieldProxy__ZeroSharesAmount(); - -/// @dev Error when the client basis points are invalid -error P2pYieldProxy__InvalidClientBasisPoints(uint96 _clientBasisPoints); - -/// @dev Error when the factory is not the caller -error P2pYieldProxy__NotFactory(address _factory); - -error P2pYieldProxy__DifferentActuallyDepositedAmount( - uint256 _requestedAmount, - uint256 _actualAmount -); - -/// @dev Error when the factory is not the caller -/// @param _msgSender sender address. -/// @param _actualFactory the actual factory address. -error P2pYieldProxy__NotFactoryCalled( - address _msgSender, - IP2pYieldProxyFactory _actualFactory -); - -/// @dev Error when the client is not the caller -/// @param _msgSender sender address. -/// @param _actualClient the actual client address. -error P2pYieldProxy__NotClientCalled( - address _msgSender, - address _actualClient -); +error P2pYieldProxy__ZeroAssetAmount(address _asset); +error P2pYieldProxy__InvalidClientBasisPointsOfDeposit(uint48 _clientBasisPointsOfDeposit); +error P2pYieldProxy__InvalidClientBasisPointsOfProfit(uint48 _clientBasisPointsOfProfit); +error P2pYieldProxy__DifferentActuallyDepositedAmount(address _asset, uint256 _requestedAmount, uint256 _actualAmount); +error P2pYieldProxy__NotFactoryCalled(address _msgSender, IP2pYieldProxyFactory _actualFactory); +error P2pYieldProxy__NotClientCalled(address _msgSender, address _actualClient); +error P2pYieldProxy__ZeroAddressFactory(); +error P2pYieldProxy__ZeroAddressP2pTreasury(); +error P2pYieldProxy__ZeroAddressYieldProtocolAddress(); +error P2pYieldProxy__ZeroAllowedCalldataChecker(); +error P2pYieldProxy__DataTooShort(); +error P2pYieldProxy__NotP2pOperator(address _msgSender); /// @title P2pYieldProxy /// @notice P2pYieldProxy is a contract that allows a client to deposit and withdraw assets from a yield protocol. -abstract contract P2pYieldProxy is - AllowedCalldataChecker, - P2pStructs, - ReentrancyGuard, - ERC165, - IP2pYieldProxy { - +abstract contract P2pYieldProxy is Initializable, ReentrancyGuardUpgradeable, ERC165, IP2pYieldProxy { using SafeERC20 for IERC20; using Address for address; + address constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @dev P2pYieldProxyFactory IP2pYieldProxyFactory internal immutable i_factory; /// @dev P2pTreasury - address internal immutable i_p2pTreasury; + address payable internal immutable i_p2pTreasury; /// @dev Yield protocol address address internal immutable i_yieldProtocolAddress; + IAllowedCalldataChecker internal immutable i_allowedCalldataChecker; + /// @dev Client - address internal s_client; + address payable internal s_client; + + /// @dev Client basis points of deposit + uint48 internal s_clientBasisPointsOfDeposit; - /// @dev Client basis points - uint96 internal s_clientBasisPoints; + /// @dev Client basis points of profit + uint48 internal s_clientBasisPointsOfProfit; - // asset => amount - mapping(address => uint256) internal s_totalDeposited; + mapping(uint256 vaultId => mapping(address asset => uint256 amount)) internal s_totalDeposited; - // asset => amount - mapping(address => uint256) internal s_totalWithdrawn; + mapping(uint256 vaultId => mapping(address asset => Withdrawn withdrawn)) internal s_totalWithdrawn; /// @notice If caller is not factory, revert modifier onlyFactory() { @@ -101,203 +76,237 @@ abstract contract P2pYieldProxy is _; } + modifier onlyP2pOperator() { + address p2pOperator = i_factory.getP2pOperator(); + if (msg.sender != p2pOperator) { + revert P2pYieldProxy__NotP2pOperator(msg.sender); + } + _; + } + + /// @dev Modifier for checking if a calldata is allowed + /// @param _yieldProtocolAddress The address of the yield protocol + /// @param _yieldProtocolCalldata The calldata (encoded signature + arguments) to be passed to the yield protocol + modifier calldataShouldBeAllowed(address _yieldProtocolAddress, bytes calldata _yieldProtocolCalldata) { + // validate yieldProtocolCalldata for yieldProtocolAddress + bytes4 selector = _getFunctionSelector(_yieldProtocolCalldata); + i_allowedCalldataChecker.checkCalldata(_yieldProtocolAddress, selector, _yieldProtocolCalldata[4:]); + _; + } + /// @notice Constructor for P2pYieldProxy /// @param _factory The factory address /// @param _p2pTreasury The P2pTreasury address /// @param _yieldProtocolAddress Yield protocol address + /// @param _allowedCalldataChecker AllowedCalldataChecker constructor( address _factory, address _p2pTreasury, - address _yieldProtocolAddress + address _yieldProtocolAddress, + address _allowedCalldataChecker ) { + require(_factory != address(0), P2pYieldProxy__ZeroAddressFactory()); i_factory = IP2pYieldProxyFactory(_factory); - i_p2pTreasury = _p2pTreasury; + + require(_p2pTreasury != address(0), P2pYieldProxy__ZeroAddressP2pTreasury()); + i_p2pTreasury = payable(_p2pTreasury); + + require(_yieldProtocolAddress != address(0), P2pYieldProxy__ZeroAddressYieldProtocolAddress()); i_yieldProtocolAddress = _yieldProtocolAddress; + + require(_allowedCalldataChecker != address(0), P2pYieldProxy__ZeroAllowedCalldataChecker()); + i_allowedCalldataChecker = IAllowedCalldataChecker(_allowedCalldataChecker); } /// @inheritdoc IP2pYieldProxy - function initialize( - address _client, - uint96 _clientBasisPoints - ) - external - onlyFactory + function initialize(address _client, uint48 _clientBasisPointsOfDeposit, uint48 _clientBasisPointsOfProfit) + external + initializer + onlyFactory { + __ReentrancyGuard_init(); + require( - _clientBasisPoints > 0 && _clientBasisPoints <= 10_000, - P2pYieldProxy__InvalidClientBasisPoints(_clientBasisPoints) + _clientBasisPointsOfDeposit <= 10_000, + P2pYieldProxy__InvalidClientBasisPointsOfDeposit(_clientBasisPointsOfDeposit) + ); + require( + _clientBasisPointsOfProfit <= 10_000, + P2pYieldProxy__InvalidClientBasisPointsOfProfit(_clientBasisPointsOfProfit) ); - s_client = _client; - s_clientBasisPoints = _clientBasisPoints; + s_client = payable(_client); + s_clientBasisPointsOfDeposit = _clientBasisPointsOfDeposit; + s_clientBasisPointsOfProfit = _clientBasisPointsOfProfit; emit P2pYieldProxy__Initialized(); } + function deposit(bytes calldata _yieldProtocolDepositCalldata) external payable virtual; + /// @notice Deposit assets into yield protocol + /// @param _vaultId vault ID + /// @param _asset ERC-20 asset address (use NATIVE sentinel for ETH) + /// @param _amount Amount of ERC-20 asset to transfer from client (ignored for native deposits) /// @param _yieldProtocolDepositCalldata calldata for deposit function of yield protocol - /// @param _permitSingleForP2pYieldProxy PermitSingle for P2pYieldProxy to pull assets from client - /// @param _permit2SignatureForP2pYieldProxy signature of PermitSingle for P2pYieldProxy - /// @param _usePermit2 whether should use Permit2 or native ERC-20 transferFrom + /// @param _isNative whether ETH (native currency) is being deposited function _deposit( + uint256 _vaultId, + address _asset, + uint256 _amount, bytes memory _yieldProtocolDepositCalldata, - IAllowanceTransfer.PermitSingle calldata _permitSingleForP2pYieldProxy, - bytes calldata _permit2SignatureForP2pYieldProxy, - bool _usePermit2 - ) - internal - onlyFactory - { - address asset = _permitSingleForP2pYieldProxy.details.token; - require (asset != address(0), P2pYieldProxy__ZeroAddressAsset()); + bool _isNative + ) internal onlyFactory { + uint256 nativeAmountToDepositAfterFee = msg.value * s_clientBasisPointsOfDeposit / 10_000; + + if (_isNative) { + uint256 totalDepositedAfter = s_totalDeposited[_vaultId][NATIVE] + nativeAmountToDepositAfterFee; + s_totalDeposited[_vaultId][NATIVE] = totalDepositedAfter; + emit P2pYieldProxy__Deposited(_vaultId, NATIVE, nativeAmountToDepositAfterFee, totalDepositedAfter); + } else { + require(_asset != address(0), P2pYieldProxy__ZeroAddressAsset()); + require(_amount > 0, P2pYieldProxy__ZeroAssetAmount(_asset)); - uint160 amount = _permitSingleForP2pYieldProxy.details.amount; - require (amount > 0, P2pYieldProxy__ZeroAssetAmount()); + address client = s_client; - address client = s_client; + uint256 assetAmountBefore = IERC20(_asset).balanceOf(address(this)); - // transfer tokens into Proxy - try Permit2Lib.PERMIT2.permit( - client, - _permitSingleForP2pYieldProxy, - _permit2SignatureForP2pYieldProxy - ) {} - catch {} // prevent unintended reverts due to invalidated nonce + // Transfer tokens from client to proxy using standard ERC20 transferFrom + // Client must have approved P2pYieldProxy to spend tokens + IERC20(_asset).safeTransferFrom(client, address(this), _amount); - uint256 assetAmountBefore = IERC20(asset).balanceOf(address(this)); + uint256 assetAmountAfter = IERC20(_asset).balanceOf(address(this)); + uint256 actualAmount = assetAmountAfter - assetAmountBefore; - Permit2Lib.PERMIT2.transferFrom( - client, - address(this), - amount, - asset - ); + require( + actualAmount == _amount, P2pYieldProxy__DifferentActuallyDepositedAmount(_asset, _amount, actualAmount) + ); // no support for fee-on-transfer or rebasing tokens - uint256 assetAmountAfter = IERC20(asset).balanceOf(address(this)); - uint256 actualAmount = assetAmountAfter - assetAmountBefore; - - require ( - actualAmount == amount, - P2pYieldProxy__DifferentActuallyDepositedAmount(amount, actualAmount) - ); // no support for fee-on-transfer or rebasing tokens - - uint256 totalDepositedAfter = s_totalDeposited[asset] + actualAmount; - s_totalDeposited[asset] = totalDepositedAfter; - emit P2pYieldProxy__Deposited( - i_yieldProtocolAddress, - asset, - actualAmount, - totalDepositedAfter - ); + uint256 amountToDepositAfterFee = actualAmount * s_clientBasisPointsOfDeposit / 10_000; - if (_usePermit2) { - IERC20(asset).safeIncreaseAllowance( - address(Permit2Lib.PERMIT2), - actualAmount - ); - } else { - IERC20(asset).safeIncreaseAllowance( - i_yieldProtocolAddress, - actualAmount - ); + uint256 totalDepositedAfter = s_totalDeposited[_vaultId][_asset] + amountToDepositAfterFee; + s_totalDeposited[_vaultId][_asset] = totalDepositedAfter; + emit P2pYieldProxy__Deposited(_vaultId, _asset, amountToDepositAfterFee, totalDepositedAfter); + + uint256 erc20FeeAmount = actualAmount - amountToDepositAfterFee; + if (erc20FeeAmount > 0) { + emit P2pYieldProxy__DepositFee(_asset, erc20FeeAmount); + IERC20(_asset).safeTransfer(i_p2pTreasury, erc20FeeAmount); + } + + // Approve yield protocol to spend tokens + IERC20(_asset).safeIncreaseAllowance(i_yieldProtocolAddress, amountToDepositAfterFee); + } + + uint256 nativeFeeAmount = msg.value - nativeAmountToDepositAfterFee; + if (nativeFeeAmount > 0) { + emit P2pYieldProxy__DepositFee(NATIVE, nativeFeeAmount); + Address.sendValue(i_p2pTreasury, nativeFeeAmount); } - i_yieldProtocolAddress.functionCall(_yieldProtocolDepositCalldata); + i_yieldProtocolAddress.functionCallWithValue(_yieldProtocolDepositCalldata, nativeAmountToDepositAfterFee); } /// @notice Withdraw assets from yield protocol + /// @param _vaultId vault ID /// @param _asset ERC-20 asset address /// @param _yieldProtocolWithdrawalCalldata calldata for withdraw function of yield protocol - function _withdraw( - address _asset, - bytes memory _yieldProtocolWithdrawalCalldata - ) - internal - onlyClient - nonReentrant + function _withdraw(uint256 _vaultId, address _asset, bytes memory _yieldProtocolWithdrawalCalldata) + internal + nonReentrant { - uint256 assetAmountBefore = IERC20(_asset).balanceOf(address(this)); + int256 accruedRewards = calculateAccruedRewards(_vaultId, _asset); + + bool isNative = _asset == NATIVE; + + uint256 assetAmountBefore = isNative ? address(this).balance : IERC20(_asset).balanceOf(address(this)); // withdraw assets from Protocol i_yieldProtocolAddress.functionCall(_yieldProtocolWithdrawalCalldata); - uint256 assetAmountAfter = IERC20(_asset).balanceOf(address(this)); + uint256 assetAmountAfter = isNative ? address(this).balance : IERC20(_asset).balanceOf(address(this)); uint256 newAssetAmount = assetAmountAfter - assetAmountBefore; - uint256 totalWithdrawnBefore = s_totalWithdrawn[_asset]; - uint256 totalWithdrawnAfter = totalWithdrawnBefore + newAssetAmount; - uint256 totalDeposited = s_totalDeposited[_asset]; - - // update total withdrawn - s_totalWithdrawn[_asset] = totalWithdrawnAfter; - - // Calculate profit increment - // profit = (total withdrawn after this - total deposited) - // If it's negative or zero, no profit yet - uint256 profitBefore; - if (totalWithdrawnBefore > totalDeposited) { - profitBefore = totalWithdrawnBefore - totalDeposited; + if (newAssetAmount == 0) { + emit P2pYieldProxy__EmergencyWithdrawalQueueFlow(_vaultId, _asset); + return; } - uint256 profitAfter; - if (totalWithdrawnAfter > totalDeposited) { - profitAfter = totalWithdrawnAfter - totalDeposited; - } - uint256 newProfit; - if (profitAfter > profitBefore) { - newProfit = profitAfter - profitBefore; + + Withdrawn memory withdrawn = s_totalWithdrawn[_vaultId][_asset]; + uint256 totalWithdrawnBefore = uint256(withdrawn.amount); + uint256 accruedRewardsPositive; + if (accruedRewards > 0) { + accruedRewardsPositive = uint256(accruedRewards); } + uint256 profitPortion = newAssetAmount > accruedRewardsPositive ? accruedRewardsPositive : newAssetAmount; + uint256 principalPortion = newAssetAmount - profitPortion; + + uint256 totalWithdrawnAfter = totalWithdrawnBefore + principalPortion; + + // update total withdrawn + withdrawn.amount = uint208(totalWithdrawnAfter); + withdrawn.lastFeeCollectionTime = uint48(block.timestamp); + s_totalWithdrawn[_vaultId][_asset] = withdrawn; + uint256 p2pAmount; - if (newProfit > 0) { + if (accruedRewards > 0) { // That extra 9999 ensures that any nonzero remainder will push the result up by 1 (ceiling division). - p2pAmount = (newProfit * (10_000 - s_clientBasisPoints) + 9999) / 10_000; + p2pAmount = calculateP2pFeeAmount(profitPortion); } uint256 clientAmount = newAssetAmount - p2pAmount; if (p2pAmount > 0) { - IERC20(_asset).safeTransfer(i_p2pTreasury, p2pAmount); + if (isNative) { + Address.sendValue(i_p2pTreasury, p2pAmount); + } else { + IERC20(_asset).safeTransfer(i_p2pTreasury, p2pAmount); + } } // clientAmount must be > 0 at this point - IERC20(_asset).safeTransfer(s_client, clientAmount); + if (isNative) { + Address.sendValue(s_client, clientAmount); + } else { + IERC20(_asset).safeTransfer(s_client, clientAmount); + } emit P2pYieldProxy__Withdrawn( - i_yieldProtocolAddress, - i_yieldProtocolAddress, - _asset, - newAssetAmount, - totalWithdrawnAfter, - newProfit, - p2pAmount, - clientAmount + _vaultId, _asset, newAssetAmount, totalWithdrawnAfter, accruedRewards, p2pAmount, clientAmount ); } /// @inheritdoc IP2pYieldProxy - function callAnyFunction( - address _yieldProtocolAddress, - bytes calldata _yieldProtocolCalldata - ) - external - onlyClient - nonReentrant - calldataShouldBeAllowed(_yieldProtocolAddress, _yieldProtocolCalldata) + function callAnyFunction(address _yieldProtocolAddress, bytes calldata _yieldProtocolCalldata) + external + onlyClient + nonReentrant + calldataShouldBeAllowed(_yieldProtocolAddress, _yieldProtocolCalldata) { emit P2pYieldProxy__CalledAsAnyFunction(_yieldProtocolAddress); _yieldProtocolAddress.functionCall(_yieldProtocolCalldata); } - /// @inheritdoc IAllowedCalldataChecker - function checkCalldata( - address _target, - bytes4 _selector, - bytes calldata _calldataAfterSelector - ) public view override(AllowedCalldataChecker, IAllowedCalldataChecker) { - i_factory.checkCalldata( - _target, - _selector, - _calldataAfterSelector - ); + /// @inheritdoc IP2pYieldProxy + function emergencyTokenWithdraw(address _token) external onlyClient nonReentrant { + uint256 amount = IERC20(_token).balanceOf(address(this)); + emit P2pYieldProxy__EmergencyWithdrawn(_token, amount); + IERC20(_token).safeTransfer(s_client, amount); + } + + /// @inheritdoc IP2pYieldProxy + function emergencyNativeWithdraw() external onlyClient nonReentrant { + uint256 amount = address(this).balance; + emit P2pYieldProxy__EmergencyWithdrawn(NATIVE, amount); + Address.sendValue(s_client, amount); + } + + /// @notice Returns function selector (first 4 bytes of data) + /// @param _data calldata (encoded signature + arguments) + /// @return functionSelector function selector + function _getFunctionSelector(bytes calldata _data) private pure returns (bytes4 functionSelector) { + require(_data.length >= 4, P2pYieldProxy__DataTooShort()); + return bytes4(_data[:4]); } /// @inheritdoc IP2pYieldProxy @@ -310,29 +319,80 @@ abstract contract P2pYieldProxy is return i_p2pTreasury; } + /// @inheritdoc IP2pYieldProxy + function getYieldProtocolAddress() external view returns (address) { + return i_yieldProtocolAddress; + } + + /// @inheritdoc IP2pYieldProxy + function getAllowedCalldataChecker() external view returns (address) { + return address(i_allowedCalldataChecker); + } + /// @inheritdoc IP2pYieldProxy function getClient() external view returns (address) { return s_client; } /// @inheritdoc IP2pYieldProxy - function getClientBasisPoints() external view returns (uint96) { - return s_clientBasisPoints; + function getClientBasisPointsOfDeposit() external view returns (uint48) { + return s_clientBasisPointsOfDeposit; + } + + /// @inheritdoc IP2pYieldProxy + function getClientBasisPointsOfProfit() external view returns (uint48) { + return s_clientBasisPointsOfProfit; + } + + /// @inheritdoc IP2pYieldProxy + function getTotalDeposited(uint256 _vaultId, address _asset) external view returns (uint256) { + return s_totalDeposited[_vaultId][_asset]; + } + + /// @inheritdoc IP2pYieldProxy + function getTotalWithdrawn(uint256 _vaultId, address _asset) external view returns (uint256) { + return s_totalWithdrawn[_vaultId][_asset].amount; } /// @inheritdoc IP2pYieldProxy - function getTotalDeposited(address _asset) external view returns (uint256) { - return s_totalDeposited[_asset]; + function getUserPrincipal(uint256 _vaultId, address _asset) public view returns (uint256) { + uint256 totalDeposited = s_totalDeposited[_vaultId][_asset]; + uint256 totalWithdrawn = s_totalWithdrawn[_vaultId][_asset].amount; + if (totalDeposited > totalWithdrawn) { + return totalDeposited - totalWithdrawn; + } + return 0; + } + + /// @inheritdoc IP2pYieldProxy + function calculateAccruedRewards(uint256 _vaultId, address _asset) public view virtual returns (int256); + + /// @inheritdoc IP2pYieldProxy + function getLastFeeCollectionTime(uint256 _vaultId, address _asset) public view returns (uint48) { + return s_totalWithdrawn[_vaultId][_asset].lastFeeCollectionTime; } /// @inheritdoc IP2pYieldProxy - function getTotalWithdrawn(address _asset) external view returns (uint256) { - return s_totalWithdrawn[_asset]; + function calculateMinAmountToApproveForDeposit(uint256 _amountToDeposit) public view returns (uint256) { + uint48 clientBasisPointsOfDeposit = s_clientBasisPointsOfDeposit; + if (clientBasisPointsOfDeposit == 0) { + return _amountToDeposit; + } + return (_amountToDeposit * 10_000 + clientBasisPointsOfDeposit - 1) / clientBasisPointsOfDeposit; } /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IP2pYieldProxy).interfaceId || - super.supportsInterface(interfaceId); + return interfaceId == type(IP2pYieldProxy).interfaceId || super.supportsInterface(interfaceId); + } + + /// @notice Calculates P2P treasury fee amount using ceiling division + /// @param _amount amount + /// @return p2pFeeAmount p2p fee amount + function calculateP2pFeeAmount(uint256 _amount) internal view returns (uint256 p2pFeeAmount) { + if (_amount == 0) { + return 0; + } + p2pFeeAmount = (_amount * (10_000 - s_clientBasisPointsOfProfit) + 9999) / 10_000; } } diff --git a/src/p2pYieldProxyFactory/IP2pYieldProxyFactory.sol b/src/p2pYieldProxyFactory/IP2pYieldProxyFactory.sol index 2102f8e..c345d0f 100644 --- a/src/p2pYieldProxyFactory/IP2pYieldProxyFactory.sol +++ b/src/p2pYieldProxyFactory/IP2pYieldProxyFactory.sol @@ -4,133 +4,89 @@ pragma solidity 0.8.27; import "../@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import "../@permit2/interfaces/IAllowanceTransfer.sol"; -import "../common/IAllowedCalldataChecker.sol"; -import "../common/P2pStructs.sol"; /// @dev External interface of P2pYieldProxyFactory -interface IP2pYieldProxyFactory is IAllowedCalldataChecker, IERC165 { - +interface IP2pYieldProxyFactory is IERC165 { /// @dev Emitted when the P2pSigner is transferred - event P2pYieldProxyFactory__P2pSignerTransferred( - address indexed _previousP2pSigner, - address indexed _newP2pSigner - ); - - /// @dev Emitted when the calldata rules are set - event P2pYieldProxyFactory__CalldataRulesSet( - address indexed _contract, - bytes4 indexed _selector, - P2pStructs.Rule[] _rules - ); - - /// @dev Emitted when the calldata rules are removed - event P2pYieldProxyFactory__CalldataRulesRemoved( - address indexed _contract, - bytes4 indexed _selector - ); + event P2pYieldProxyFactory__P2pSignerTransferred(address indexed _previousP2pSigner, address indexed _newP2pSigner); /// @dev Emitted when the deposit is made event P2pYieldProxyFactory__Deposited( - address indexed _client, - uint96 indexed _clientBasisPoints + address indexed _client, uint48 indexed _clientBasisPointsOfDeposit, uint48 indexed _clientBasisPointsOfProfit ); - /// @dev Deposits the yield protocol - /// @param _permitSingleForP2pYieldProxy The permit single for P2pYieldProxy - /// @param _permit2SignatureForP2pYieldProxy The permit2 signature for P2pYieldProxy - /// @param _clientBasisPoints The client basis points - /// @param _p2pSignerSigDeadline The P2pSigner signature deadline - /// @param _p2pSignerSignature The P2pSigner signature - /// @return p2pYieldProxyAddress The client's P2pYieldProxy instance address - function deposit( - IAllowanceTransfer.PermitSingle memory _permitSingleForP2pYieldProxy, - bytes calldata _permit2SignatureForP2pYieldProxy, + /// @dev Emitted when the a new proxy is created + event P2pYieldProxyFactory__ProxyCreated( + address _proxy, address _client, uint48 _clientBasisPointsOfDeposit, uint48 _clientBasisPointsOfProfit + ); - uint96 _clientBasisPoints, + /// @notice Initiates a deposit through a client specific P2pYieldProxy instance + /// @param _yieldProtocolCalldata Calldata that executes the deposit on the underlying yield protocol + /// @param _clientBasisPointsOfDeposit Client share of the deposited principal in basis points (max 10_000) + /// @param _clientBasisPointsOfProfit Client share of the generated profit in basis points (max 10_000) + /// @param _p2pSignerSigDeadline Expiration timestamp for the P2pSigner signature + /// @param _p2pSignerSignature Signature issued by the P2pSigner authorising the deposit parameters + /// @return p2pYieldProxyAddress The address of the client specific P2pYieldProxy used for the deposit + function deposit( + bytes calldata _yieldProtocolCalldata, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, uint256 _p2pSignerSigDeadline, bytes calldata _p2pSignerSignature - ) - external - returns (address p2pYieldProxyAddress); - - /// @dev Sets the calldata rules - /// @param _contract The contract address - /// @param _selector The selector - /// @param _rules The rules - function setCalldataRules( - address _contract, - bytes4 _selector, - P2pStructs.Rule[] calldata _rules - ) external; - - /// @dev Removes the calldata rules - /// @param _contract The contract address - /// @param _selector The selector - function removeCalldataRules( - address _contract, - bytes4 _selector - ) external; - - /// @dev Computes the address of a P2pYieldProxy created by `_createP2pYieldProxy` function - /// @dev P2pYieldProxy instances are guaranteed to have the same address if _feeDistributorInstance is the same - /// @param _client The address of client - /// @return address The address of the P2pYieldProxy instance + ) external payable returns (address p2pYieldProxyAddress); + + /// @notice Computes the deterministic address of a P2pYieldProxy for a client and fee configuration + /// @param _client Client wallet address + /// @param _clientBasisPointsOfDeposit Client share of the deposited principal in basis points (max 10_000) + /// @param _clientBasisPointsOfProfit Client share of the generated profit in basis points (max 10_000) + /// @return Address of the P2pYieldProxy instance that would be deployed for the provided parameters function predictP2pYieldProxyAddress( address _client, - uint96 _clientBasisPoints + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit ) external view returns (address); - /// @dev Transfers the P2pSigner - /// @param _newP2pSigner The new P2pSigner address - function transferP2pSigner( - address _newP2pSigner - ) external; + /// @notice Updates the P2pSigner account that authorises deposits + /// @param _newP2pSigner Address of the new P2pSigner + function transferP2pSigner(address _newP2pSigner) external; + + /// @notice Transfers P2pOperator role control to a new account using the two step flow + /// @param _newP2pOperator Address that will become the new P2pOperator upon acceptance + function transferP2pOperator(address _newP2pOperator) external; - /// @dev Returns a template set by P2P to be used for new P2pYieldProxy instances - /// @return a template set by P2P to be used for new P2pYieldProxy instances + /// @notice Finalises a pending two step P2pOperator transfer + function acceptP2pOperator() external; + + /// @notice Returns the address that is set to become the next P2pOperator + /// @return pendingP2pOperator Address of the pending P2pOperator + function getPendingP2pOperator() external view returns (address pendingP2pOperator); + + /// @notice Returns the reference implementation used for cloning new P2pYieldProxy instances + /// @return Address of the reference P2pYieldProxy implementation function getReferenceP2pYieldProxy() external view returns (address); - /// @dev Gets the hash for the P2pSigner - /// @param _client The address of client - /// @param _clientBasisPoints The client basis points - /// @param _p2pSignerSigDeadline The P2pSigner signature deadline - /// @return The hash for the P2pSigner + /// @notice Computes the message hash that must be signed by the P2pSigner for a deposit authorisation + /// @param _client Client wallet initiating the deposit + /// @param _clientBasisPointsOfDeposit Client share of the deposited principal in basis points (max 10_000) + /// @param _clientBasisPointsOfProfit Client share of the generated profit in basis points (max 10_000) + /// @param _p2pSignerSigDeadline Expiration timestamp for the signature + /// @return hash Message hash to be signed by the P2pSigner function getHashForP2pSigner( address _client, - uint96 _clientBasisPoints, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, uint256 _p2pSignerSigDeadline ) external view returns (bytes32); - /// @dev Gets the permit2 hash typed data - /// @param _permitSingle The permit single - /// @return The permit2 hash typed data - function getPermit2HashTypedData(IAllowanceTransfer.PermitSingle calldata _permitSingle) external view returns (bytes32); - - /// @dev Gets the permit2 hash typed data - /// @param _permitHash The permit hash - /// @return The permit2 hash typed data - function getPermit2HashTypedData(bytes32 _permitHash) external view returns (bytes32); - - /// @dev Gets the permit hash - /// @param _permitSingle The permit single - /// @return The permit hash - function getPermitHash(IAllowanceTransfer.PermitSingle calldata _permitSingle) external view returns (bytes32); - - /// @dev Gets the calldata rules - /// @param _contract The contract address - /// @param _selector The selector - /// @return The calldata rules - function getCalldataRules( - address _contract, - bytes4 _selector - ) external view returns (P2pStructs.Rule[] memory); - - /// @dev Gets the P2pSigner - /// @return The P2pSigner address + /// @notice Returns the current P2pSigner address authorised to validate deposits + /// @return Address of the P2pSigner function getP2pSigner() external view returns (address); - /// @dev Gets all proxies - /// @return The proxy addresses + /// @notice Returns the current P2pOperator address responsible for administrative actions + /// @return Address of the P2pOperator + function getP2pOperator() external view returns (address); + + /// @notice Returns the list of all P2pYieldProxy instances created by the factory + /// @return Array of deployed P2pYieldProxy addresses function getAllProxies() external view returns (address[] memory); } diff --git a/src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol b/src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol index 3b83ea9..486fc79 100644 --- a/src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol +++ b/src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol @@ -7,11 +7,7 @@ import "../@openzeppelin/contracts/proxy/Clones.sol"; import "../@openzeppelin/contracts/utils/Address.sol"; import "../@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import "../@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "../@permit2/interfaces/IAllowanceTransfer.sol"; -import "../@permit2/libraries/PermitHash.sol"; import "../access/P2pOperator2Step.sol"; -import "../common/AllowedCalldataChecker.sol"; -import "../common/P2pStructs.sol"; import "../p2pYieldProxy/P2pYieldProxy.sol"; import "./IP2pYieldProxyFactory.sol"; @@ -22,69 +18,19 @@ error P2pYieldProxyFactory__ZeroP2pSignerAddress(); error P2pYieldProxyFactory__InvalidP2pSignerSignature(); /// @dev Error when the P2pSigner signature is expired -error P2pYieldProxyFactory__P2pSignerSignatureExpired( - uint256 _p2pSignerSigDeadline -); - -/// @dev Error when no rules are defined -error P2pYieldProxyFactory__NoRulesDefined( - address _target, - bytes4 _selector -); - -/// @dev Error when no calldata is allowed -error P2pYieldProxyFactory__NoCalldataAllowed( - address _target, - bytes4 _selector -); - -/// @dev Error when the calldata is too short for the start with rule -error P2pYieldProxyFactory__CalldataTooShortForStartsWithRule( - uint256 _calldataAfterSelectorLength, - uint32 _ruleIndex, - uint32 _bytesCount -); - -/// @dev Error when the calldata starts with rule is violated -error P2pYieldProxyFactory__CalldataStartsWithRuleViolated( - bytes _actual, - bytes _expected -); - -/// @dev Error when the calldata is too short for the ends with rule -error P2pYieldProxyFactory__CalldataTooShortForEndsWithRule( - uint256 _calldataAfterSelectorLength, - uint32 _bytesCount -); - -/// @dev Error when the calldata ends with rule is violated -error P2pYieldProxyFactory__CalldataEndsWithRuleViolated( - bytes _actual, - bytes _expected -); +error P2pYieldProxyFactory__P2pSignerSignatureExpired(uint256 _p2pSignerSigDeadline); /// @title P2pYieldProxyFactory /// @author P2P Validator /// @notice P2pYieldProxyFactory is a factory contract for creating P2pYieldProxy contracts -abstract contract P2pYieldProxyFactory is - AllowedCalldataChecker, - P2pOperator2Step, - P2pStructs, - ERC165, - IP2pYieldProxyFactory { - - using SafeCast160 for uint256; +abstract contract P2pYieldProxyFactory is P2pOperator2Step, ERC165, IP2pYieldProxyFactory { using SignatureChecker for address; using ECDSA for bytes32; /// @notice Reference P2pYieldProxy contract P2pYieldProxy internal immutable i_referenceP2pYieldProxy; - // Contract => Selector => Rule[] - // all rules must be followed for (Contract, Selector) - mapping(address => mapping(bytes4 => Rule[])) internal s_calldataRules; - - /// @notice P2pSigner address + /// @notice P2pSigner address address internal s_p2pSigner; /// @notice All proxies @@ -92,8 +38,8 @@ abstract contract P2pYieldProxyFactory is /// @notice Modifier to check if the P2pSigner signature should not expire modifier p2pSignerSignatureShouldNotExpire(uint256 _p2pSignerSigDeadline) { - require ( - block.timestamp < _p2pSignerSigDeadline, + require( + block.timestamp <= _p2pSignerSigDeadline, P2pYieldProxyFactory__P2pSignerSignatureExpired(_p2pSignerSigDeadline) ); _; @@ -101,18 +47,17 @@ abstract contract P2pYieldProxyFactory is /// @notice Modifier to check if the P2pSigner signature should be valid modifier p2pSignerSignatureShouldBeValid( - uint96 _clientBasisPoints, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, uint256 _p2pSignerSigDeadline, bytes calldata _p2pSignerSignature ) { - require ( + require( s_p2pSigner.isValidSignatureNow( - getHashForP2pSigner( - msg.sender, - _clientBasisPoints, - _p2pSignerSigDeadline + getHashForP2pSigner( + msg.sender, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _p2pSignerSigDeadline ).toEthSignedMessageHash(), - _p2pSignerSignature + _p2pSignerSignature ), P2pYieldProxyFactory__InvalidP2pSignerSignature() ); @@ -121,197 +66,128 @@ abstract contract P2pYieldProxyFactory is /// @notice Constructor for P2pYieldProxyFactory /// @param _p2pSigner The P2pSigner address - constructor( - address _p2pSigner - ) P2pOperator(msg.sender) { + /// @param _p2pOperator The P2pOperator address + constructor(address _p2pSigner, address _p2pOperator) P2pOperator(_p2pOperator) { _transferP2pSigner(_p2pSigner); } /// @inheritdoc IP2pYieldProxyFactory - function transferP2pSigner( - address _newP2pSigner - ) external onlyP2pOperator { + function transferP2pSigner(address _newP2pSigner) external onlyP2pOperator { _transferP2pSigner(_newP2pSigner); } /// @inheritdoc IP2pYieldProxyFactory - function setCalldataRules( - address _contract, - bytes4 _selector, - Rule[] calldata _rules - ) external onlyP2pOperator { - s_calldataRules[_contract][_selector] = _rules; - emit P2pYieldProxyFactory__CalldataRulesSet( - _contract, - _selector, - _rules - ); + function transferP2pOperator(address _newP2pOperator) + public + virtual + override(P2pOperator2Step, IP2pYieldProxyFactory) + { + P2pOperator2Step.transferP2pOperator(_newP2pOperator); } /// @inheritdoc IP2pYieldProxyFactory - function removeCalldataRules( - address _contract, - bytes4 _selector - ) external onlyP2pOperator { - delete s_calldataRules[_contract][_selector]; - emit P2pYieldProxyFactory__CalldataRulesRemoved( - _contract, - _selector - ); + function acceptP2pOperator() public virtual override(P2pOperator2Step, IP2pYieldProxyFactory) { + P2pOperator2Step.acceptP2pOperator(); } /// @inheritdoc IP2pYieldProxyFactory - function deposit( - IAllowanceTransfer.PermitSingle memory _permitSingleForP2pYieldProxy, - bytes calldata _permit2SignatureForP2pYieldProxy, + function getPendingP2pOperator() + public + view + virtual + override(P2pOperator2Step, IP2pYieldProxyFactory) + returns (address) + { + return P2pOperator2Step.getPendingP2pOperator(); + } - uint96 _clientBasisPoints, + /// @inheritdoc IP2pYieldProxyFactory + function deposit( + bytes calldata _yieldProtocolCalldata, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, uint256 _p2pSignerSigDeadline, bytes calldata _p2pSignerSignature ) - external - p2pSignerSignatureShouldNotExpire(_p2pSignerSigDeadline) - p2pSignerSignatureShouldBeValid(_clientBasisPoints, _p2pSignerSigDeadline, _p2pSignerSignature) - returns (address p2pYieldProxyAddress) + external + payable + p2pSignerSignatureShouldNotExpire(_p2pSignerSigDeadline) + p2pSignerSignatureShouldBeValid( + _clientBasisPointsOfDeposit, + _clientBasisPointsOfProfit, + _p2pSignerSigDeadline, + _p2pSignerSignature + ) + returns (address p2pYieldProxyAddress) { // create proxy if not created yet - P2pYieldProxy p2pYieldProxy = _getOrCreateP2pYieldProxy(_clientBasisPoints); + P2pYieldProxy p2pYieldProxy = _getOrCreateP2pYieldProxy(_clientBasisPointsOfDeposit, _clientBasisPointsOfProfit); // deposit via proxy - p2pYieldProxy.deposit( - _permitSingleForP2pYieldProxy, - _permit2SignatureForP2pYieldProxy - ); + p2pYieldProxy.deposit{value: msg.value}(_yieldProtocolCalldata); - emit P2pYieldProxyFactory__Deposited(msg.sender, _clientBasisPoints); + emit P2pYieldProxyFactory__Deposited(msg.sender, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit); p2pYieldProxyAddress = address(p2pYieldProxy); } - function _transferP2pSigner( - address _newP2pSigner - ) private { - require (_newP2pSigner != address(0), P2pYieldProxyFactory__ZeroP2pSignerAddress()); + function _transferP2pSigner(address _newP2pSigner) private { + require(_newP2pSigner != address(0), P2pYieldProxyFactory__ZeroP2pSignerAddress()); emit P2pYieldProxyFactory__P2pSignerTransferred(s_p2pSigner, _newP2pSigner); s_p2pSigner = _newP2pSigner; } /// @notice Creates a new P2pYieldProxy contract instance if not created yet - function _getOrCreateP2pYieldProxy(uint96 _clientBasisPoints) - private - returns (P2pYieldProxy p2pYieldProxy) + function _getOrCreateP2pYieldProxy(uint48 _clientBasisPointsOfDeposit, uint48 _clientBasisPointsOfProfit) + private + returns (P2pYieldProxy p2pYieldProxy) { - address p2pYieldProxyAddress = predictP2pYieldProxyAddress( - msg.sender, - _clientBasisPoints - ); + address p2pYieldProxyAddress = + predictP2pYieldProxyAddress(msg.sender, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit); uint256 codeSize = p2pYieldProxyAddress.code.length; if (codeSize > 0) { return P2pYieldProxy(p2pYieldProxyAddress); } p2pYieldProxy = P2pYieldProxy( - Clones.cloneDeterministic( + Clones.cloneDeterministic( address(i_referenceP2pYieldProxy), - _getSalt( - msg.sender, - _clientBasisPoints - ) + _getSalt(msg.sender, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit) ) ); - p2pYieldProxy.initialize( - msg.sender, - _clientBasisPoints - ); + p2pYieldProxy.initialize(msg.sender, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit); s_allProxies.push(address(p2pYieldProxy)); + + emit P2pYieldProxyFactory__ProxyCreated( + address(p2pYieldProxy), msg.sender, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit + ); } /// @notice Calculates the salt required for deterministic clone creation /// depending on client address and client basis points /// @param _clientAddress address - /// @param _clientBasisPoints basis points (10000 = 100%) + /// @param _clientBasisPointsOfDeposit The client basis points (10000 = 100%) of deposit + /// @param _clientBasisPointsOfProfit The client basis points (10000 = 100%) of profit /// @return bytes32 salt - function _getSalt( - address _clientAddress, - uint96 _clientBasisPoints - ) private pure returns (bytes32) + function _getSalt(address _clientAddress, uint48 _clientBasisPointsOfDeposit, uint48 _clientBasisPointsOfProfit) + private + pure + returns (bytes32) { - return keccak256(abi.encode(_clientAddress, _clientBasisPoints)); - } - - /// @inheritdoc IAllowedCalldataChecker - function checkCalldata( - address _target, - bytes4 _selector, - bytes calldata _calldataAfterSelector - ) public view override(AllowedCalldataChecker, IAllowedCalldataChecker) { - Rule[] memory rules = s_calldataRules[_target][_selector]; - require ( - rules.length > 0, - P2pYieldProxyFactory__NoRulesDefined(_target, _selector) - ); - - for (uint256 i = 0; i < rules.length; i++) { - Rule memory rule = rules[i]; - RuleType ruleType = rule.ruleType; - - require ( - ruleType != RuleType.None || _calldataAfterSelector.length == 0, - P2pYieldProxyFactory__NoCalldataAllowed(_target, _selector) - ); - - uint32 bytesCount = uint32(rule.allowedBytes.length); - if (ruleType == RuleType.StartsWith) { - // Ensure the calldata is at least as long as the range defined by startIndex and bytesCount - require ( - _calldataAfterSelector.length >= rule.index + bytesCount, - P2pYieldProxyFactory__CalldataTooShortForStartsWithRule( - _calldataAfterSelector.length, - rule.index, - bytesCount - ) - ); - // Compare the specified range in the calldata with the allowed bytes - require ( - keccak256(_calldataAfterSelector[rule.index:rule.index + bytesCount]) == keccak256(rule.allowedBytes), - P2pYieldProxyFactory__CalldataStartsWithRuleViolated( - _calldataAfterSelector[rule.index:rule.index + bytesCount], - rule.allowedBytes - ) - ); - } - if (ruleType == RuleType.EndsWith) { - // Ensure the calldata is at least as long as bytesCount - require ( - _calldataAfterSelector.length >= bytesCount, - P2pYieldProxyFactory__CalldataTooShortForEndsWithRule( - _calldataAfterSelector.length, - bytesCount - ) - ); - // Compare the end of the calldata with the allowed bytes - require ( - keccak256(_calldataAfterSelector[_calldataAfterSelector.length - bytesCount:]) == keccak256(rule.allowedBytes), - P2pYieldProxyFactory__CalldataEndsWithRuleViolated( - _calldataAfterSelector[_calldataAfterSelector.length - bytesCount:], - rule.allowedBytes - ) - ); - } - // if (ruleType == RuleType.AnyCalldata) do nothing - } + return keccak256(abi.encode(_clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit)); } /// @inheritdoc IP2pYieldProxyFactory function predictP2pYieldProxyAddress( address _client, - uint96 _clientBasisPoints + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit ) public view returns (address) { return Clones.predictDeterministicAddress( address(i_referenceP2pYieldProxy), - _getSalt(_client, _clientBasisPoints) + _getSalt(_client, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit) ); } @@ -320,42 +196,28 @@ abstract contract P2pYieldProxyFactory is return address(i_referenceP2pYieldProxy); } + /// @inheritdoc IP2pYieldProxyFactory + function getP2pOperator() public view virtual override(IP2pYieldProxyFactory, P2pOperator) returns (address) { + return P2pOperator.getP2pOperator(); + } + /// @inheritdoc IP2pYieldProxyFactory function getHashForP2pSigner( address _client, - uint96 _clientBasisPoints, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, uint256 _p2pSignerSigDeadline ) public view returns (bytes32) { - return keccak256(abi.encode( - _client, - _clientBasisPoints, - _p2pSignerSigDeadline, - address(this), - block.chainid - )); - } - - /// @inheritdoc IP2pYieldProxyFactory - function getPermit2HashTypedData(IAllowanceTransfer.PermitSingle calldata _permitSingle) external view returns (bytes32) { - return getPermit2HashTypedData(getPermitHash(_permitSingle)); - } - - /// @inheritdoc IP2pYieldProxyFactory - function getPermit2HashTypedData(bytes32 _dataHash) public view returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x01", Permit2Lib.PERMIT2.DOMAIN_SEPARATOR(), _dataHash)); - } - - /// @inheritdoc IP2pYieldProxyFactory - function getPermitHash(IAllowanceTransfer.PermitSingle calldata _permitSingle) public pure returns (bytes32) { - return PermitHash.hash(_permitSingle); - } - - /// @inheritdoc IP2pYieldProxyFactory - function getCalldataRules( - address _contract, - bytes4 _selector - ) external view returns (Rule[] memory) { - return s_calldataRules[_contract][_selector]; + return keccak256( + abi.encode( + _client, + _clientBasisPointsOfDeposit, + _clientBasisPointsOfProfit, + _p2pSignerSigDeadline, + address(this), + block.chainid + ) + ); } /// @inheritdoc IP2pYieldProxyFactory @@ -370,7 +232,6 @@ abstract contract P2pYieldProxyFactory is /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IP2pYieldProxyFactory).interfaceId || - super.supportsInterface(interfaceId); + return interfaceId == type(IP2pYieldProxyFactory).interfaceId || super.supportsInterface(interfaceId); } } diff --git a/src/structs/P2pStructs.sol b/src/structs/P2pStructs.sol new file mode 100644 index 0000000..9891e0b --- /dev/null +++ b/src/structs/P2pStructs.sol @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +/// @dev 256 bit struct +/// @member Amount +/// @member lastFeeCollectionTime Last Fee Collection Time +struct Withdrawn { + uint208 amount; + uint48 lastFeeCollectionTime; +} diff --git a/test/BaseMorphoUSDC.t.sol b/test/BaseMorphoUSDC.t.sol new file mode 100644 index 0000000..71f4257 --- /dev/null +++ b/test/BaseMorphoUSDC.t.sol @@ -0,0 +1,272 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../src/@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../src/access/P2pOperator.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import "../src/common/AllowedCalldataChecker.sol"; +import "../src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol"; +import "../src/p2pYieldProxy/IP2pYieldProxy.sol"; +import "forge-std/Test.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import "forge-std/console2.sol"; + +contract BaseMorphoUSDC is Test { + using SafeERC20 for IERC20; + + address constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant RewardsDistributor = 0xce23bD7205bF2B543F6B4eeC00Add0C111FEFc3B; + + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + + uint256 constant SuperformId = 53060340969225753329461353767745054384708953976330005872281754; + + P2pSuperformProxyFactory private factory; + + address private clientAddress; + uint256 private clientPrivateKey; + + address private p2pSignerAddress; + uint256 private p2pSignerPrivateKey; + + address private p2pOperatorAddress; + address private nobody; + + uint256 constant SigDeadline = 1742805206; + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10_000; // 0% fee + uint256 constant DepositAmount = 1234568; + uint256 constant SharesAmount = 1222092; + + uint256 DepositAmountWithFee; + + address proxyAddress; + + function setUp() public { + vm.createSelectFork("base", 27412018); + + (clientAddress, clientPrivateKey) = makeAddrAndKey("client"); + (p2pSignerAddress, p2pSignerPrivateKey) = makeAddrAndKey("p2pSigner"); + p2pOperatorAddress = makeAddr("p2pOperator"); + nobody = makeAddr("nobody"); + + vm.startPrank(p2pOperatorAddress); + + AllowedCalldataChecker implementation = new AllowedCalldataChecker(); + ProxyAdmin admin = new ProxyAdmin(); + bytes memory initData = abi.encodeWithSelector(AllowedCalldataChecker.initialize.selector); + TransparentUpgradeableProxy tup = + new TransparentUpgradeableProxy(address(implementation), address(admin), initData); + factory = new P2pSuperformProxyFactory( + p2pSignerAddress, + p2pOperatorAddress, + P2pTreasury, + SuperformRouter, + SuperPositions, + address(tup), + RewardsDistributor + ); + + vm.stopPrank(); + + proxyAddress = + factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + DepositAmountWithFee = DepositAmount + DepositAmount * (10_000 - ClientBasisPointsOfDeposit) / 10_000; + } + + function test_happyPath_Morpho() public { + deal(USDC, clientAddress, 10000e18); + + uint256 assetBalanceBefore = IERC20(USDC).balanceOf(clientAddress); + + _doDeposit(); + + uint256 assetBalanceAfter1 = IERC20(USDC).balanceOf(clientAddress); + assertEq(assetBalanceBefore - assetBalanceAfter1, DepositAmountWithFee); + + _doDeposit(); + + uint256 assetBalanceAfter2 = IERC20(USDC).balanceOf(clientAddress); + assertEq(assetBalanceAfter1 - assetBalanceAfter2, DepositAmountWithFee); + + _doDeposit(); + _doDeposit(); + + uint256 assetBalanceAfterAllDeposits = IERC20(USDC).balanceOf(clientAddress); + + _doWithdraw(10); + + uint256 assetBalanceAfterWithdraw1 = IERC20(USDC).balanceOf(clientAddress); + + assertApproxEqAbs( + assetBalanceAfterWithdraw1 - assetBalanceAfterAllDeposits, DepositAmountWithFee * 4 / 10, 10000 + ); + + _doWithdraw(5); + _doWithdraw(3); + } + + function test_depositWithZeroDepositBpsDoesNotRevertAndUsesRawAmount() public { + deal(USDC, clientAddress, 10000e18); + + address zeroBpsProxy = + factory.predictP2pYieldProxyAddress(clientAddress, 0, ClientBasisPointsOfProfit); + + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, 0, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + if (IERC20(USDC).allowance(clientAddress, zeroBpsProxy) == 0) { + IERC20(USDC).safeApprove(zeroBpsProxy, type(uint256).max); + } + + LiqRequest memory liqRequest = LiqRequest({ + txData: "", + token: USDC, + interimToken: address(0), + bridgeId: 1, + liqDstChainId: 0, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: DepositAmount, + outputAmount: SharesAmount, + maxSlippage: 50, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: zeroBpsProxy, + receiverAddressSP: zeroBpsProxy, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + // Prevent upstream router revert due to allowance expectations; we only assert proxy math here. + vm.mockCall( + SuperformRouter, + abi.encodeWithSelector(IBaseRouter.singleDirectSingleVaultDeposit.selector), + bytes("") + ); + + factory.deposit(superformCalldata, 0, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature); + vm.stopPrank(); + + IP2pYieldProxy proxy = IP2pYieldProxy(zeroBpsProxy); + assertEq(proxy.getClientBasisPointsOfDeposit(), 0); + assertEq(proxy.calculateMinAmountToApproveForDeposit(DepositAmount), DepositAmount); + } + + function _getVaultAddress() private pure returns (address) { + return address(uint160(SuperformId)); + } + + function _getP2pSignerSignature( + address _clientAddress, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, + uint256 _sigDeadline + ) private view returns (bytes memory) { + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + _clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _sigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(p2pSignerPrivateKey, ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + return p2pSignerSignature; + } + + function _doDeposit() private { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + // Approve the proxy to spend USDC tokens + if (IERC20(USDC).allowance(clientAddress, proxyAddress) == 0) { + IERC20(USDC).safeApprove(proxyAddress, type(uint256).max); + } + + LiqRequest memory liqRequest = LiqRequest({ + txData: "", + token: USDC, + interimToken: address(0), + bridgeId: 1, + liqDstChainId: 0, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: DepositAmount, + outputAmount: SharesAmount, + maxSlippage: 50, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function _doWithdraw(uint256 denominator) private { + uint256 sharesBalance = IERC1155(SuperPositions).balanceOf(proxyAddress, SuperformId); + console.log("sharesBalance"); + console.log(sharesBalance); + + uint256 sharesToWithdraw = sharesBalance / denominator; + + LiqRequest memory liqRequest = LiqRequest({ + txData: "", + token: address(0), + interimToken: address(0), + bridgeId: 1, + liqDstChainId: 0, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: DepositAmount * 4 / denominator, + outputAmount: sharesToWithdraw, + maxSlippage: 50, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultWithdraw, (req)); + + vm.startPrank(clientAddress); + P2pSuperformProxy(payable(proxyAddress)).withdraw(superformCalldata); + vm.stopPrank(); + } + + /// @dev Rolls & warps the given number of blocks forward the blockchain. + function _forward(uint256 blocks) internal { + vm.roll(block.number + blocks); + vm.warp(block.timestamp + blocks * 13); + } +} diff --git a/test/BaseMorphoUSDCDepositFee.t.sol b/test/BaseMorphoUSDCDepositFee.t.sol new file mode 100644 index 0000000..7a862b2 --- /dev/null +++ b/test/BaseMorphoUSDCDepositFee.t.sol @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../src/@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../src/access/P2pOperator.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import "../src/common/AllowedCalldataChecker.sol"; +import "../src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol"; +import "forge-std/Test.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import "forge-std/console2.sol"; + +contract BaseMorphoUSDCDepositFee is Test { + using SafeERC20 for IERC20; + + address constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant RewardsDistributor = 0xce23bD7205bF2B543F6B4eeC00Add0C111FEFc3B; + + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + + uint256 constant SuperformId = 53060340969225753329461353767745054384708953976330005872281754; + + P2pSuperformProxyFactory private factory; + + address private clientAddress; + uint256 private clientPrivateKey; + + address private p2pSignerAddress; + uint256 private p2pSignerPrivateKey; + + address private p2pOperatorAddress; + address private nobody; + + uint256 constant SigDeadline = 1742805206; + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 9500; // 5% fee + uint256 constant DepositAmount = 1234568; + uint256 constant SharesAmount = 1222092; + + uint256 DepositAmountWithFee; + + address proxyAddress; + + function setUp() public { + vm.createSelectFork("base", 27412018); + + (clientAddress, clientPrivateKey) = makeAddrAndKey("client"); + (p2pSignerAddress, p2pSignerPrivateKey) = makeAddrAndKey("p2pSigner"); + p2pOperatorAddress = makeAddr("p2pOperator"); + nobody = makeAddr("nobody"); + + vm.startPrank(p2pOperatorAddress); + + AllowedCalldataChecker implementation = new AllowedCalldataChecker(); + ProxyAdmin admin = new ProxyAdmin(); + bytes memory initData = abi.encodeWithSelector(AllowedCalldataChecker.initialize.selector); + TransparentUpgradeableProxy tup = + new TransparentUpgradeableProxy(address(implementation), address(admin), initData); + factory = new P2pSuperformProxyFactory( + p2pSignerAddress, + p2pOperatorAddress, + P2pTreasury, + SuperformRouter, + SuperPositions, + address(tup), + RewardsDistributor + ); + + vm.stopPrank(); + + proxyAddress = + factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + uint256 numerator = DepositAmount * 10_000; + uint256 denominator = ClientBasisPointsOfDeposit; + // this rounds UP instead of down + DepositAmountWithFee = (numerator + denominator - 1) / denominator; + } + + function test_happyPath_Morpho_with_deposit_fee() public { + deal(USDC, clientAddress, 10000e18); + + uint256 assetBalanceBefore = IERC20(USDC).balanceOf(clientAddress); + uint256 P2pTreasuryassetBalanceBefore = IERC20(USDC).balanceOf(P2pTreasury); + + _doDeposit(); + + uint256 P2pTreasuryassetBalanceAfter1Deposit = IERC20(USDC).balanceOf(P2pTreasury); + assertEq( + P2pTreasuryassetBalanceAfter1Deposit - P2pTreasuryassetBalanceBefore, (DepositAmountWithFee - DepositAmount) + ); + + uint256 assetBalanceAfter1 = IERC20(USDC).balanceOf(clientAddress); + assertEq(assetBalanceBefore - assetBalanceAfter1, DepositAmountWithFee); + + _doDeposit(); + + uint256 assetBalanceAfter2 = IERC20(USDC).balanceOf(clientAddress); + assertEq(assetBalanceAfter1 - assetBalanceAfter2, DepositAmountWithFee); + + _doDeposit(); + _doDeposit(); + + uint256 P2pTreasuryassetBalanceAfterAllDeposits = IERC20(USDC).balanceOf(P2pTreasury); + assertEq( + P2pTreasuryassetBalanceAfterAllDeposits - P2pTreasuryassetBalanceBefore, + (DepositAmountWithFee - DepositAmount) * 4 + ); + + _doWithdraw(10); + + assertEq(IERC20(USDC).balanceOf(proxyAddress), 0); + + _doWithdraw(5); + _doWithdraw(3); + } + + function _getVaultAddress() private pure returns (address) { + return address(uint160(SuperformId)); + } + + function _getP2pSignerSignature( + address _clientAddress, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, + uint256 _sigDeadline + ) private view returns (bytes memory) { + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + _clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _sigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(p2pSignerPrivateKey, ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + return p2pSignerSignature; + } + + function _doDeposit() private { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + // Approve the proxy to spend USDC tokens + if (IERC20(USDC).allowance(clientAddress, proxyAddress) == 0) { + IERC20(USDC).safeApprove(proxyAddress, type(uint256).max); + } + + LiqRequest memory liqRequest = LiqRequest({ + txData: "", + token: USDC, + interimToken: address(0), + bridgeId: 1, + liqDstChainId: 0, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: DepositAmount, + outputAmount: SharesAmount, + maxSlippage: 50, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function _doWithdraw(uint256 denominator) private { + uint256 sharesBalance = IERC1155(SuperPositions).balanceOf(proxyAddress, SuperformId); + + uint256 sharesToWithdraw = sharesBalance / denominator; + + LiqRequest memory liqRequest = LiqRequest({ + txData: "", + token: address(0), + interimToken: address(0), + bridgeId: 1, + liqDstChainId: 0, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: DepositAmount * 4 / denominator, + outputAmount: sharesToWithdraw, + maxSlippage: 50, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultWithdraw, (req)); + + vm.startPrank(clientAddress); + P2pSuperformProxy(payable(proxyAddress)).withdraw(superformCalldata); + vm.stopPrank(); + } + + /// @dev Rolls & warps the given number of blocks forward the blockchain. + function _forward(uint256 blocks) internal { + vm.roll(block.number + blocks); + vm.warp(block.timestamp + blocks * 13); + } +} diff --git a/test/MainnetIntegration.sol b/test/MainnetIntegration.sol deleted file mode 100644 index 952014a..0000000 --- a/test/MainnetIntegration.sol +++ /dev/null @@ -1,1045 +0,0 @@ -// SPDX-FileCopyrightText: 2025 P2P Validator -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; - -import "../src/@openzeppelin/contracts/interfaces/IERC4626.sol"; -import "../src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "../src/access/P2pOperator.sol"; -import "../src/adapters/ethena/p2pEthenaProxyFactory/P2pEthenaProxyFactory.sol"; -import "../src/common/P2pStructs.sol"; -import "../src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol"; -import "forge-std/Test.sol"; -import "forge-std/Vm.sol"; -import "forge-std/console.sol"; -import "forge-std/console2.sol"; -import {PermitHash} from "../src/@permit2/libraries/PermitHash.sol"; - - -contract MainnetIntegration is Test { - using SafeERC20 for IERC20; - - address constant USDe = 0x4c9EDD5852cd905f086C759E8383e09bff1E68B3; - address constant sUSDe = 0x9D39A5DE30e57443BfF2A8307A4256c8797A3497; - address constant P2pTreasury = 0xfeef177E6168F9b7fd59e6C5b6c2d87FF398c6FD; - - P2pEthenaProxyFactory private factory; - - address private clientAddress; - uint256 private clientPrivateKey; - - address private p2pSignerAddress; - uint256 private p2pSignerPrivateKey; - - address private p2pOperatorAddress; - address private nobody; - - uint256 constant SigDeadline = 1734464723; - uint96 constant ClientBasisPoints = 8700; // 13% fee - uint256 constant DepositAmount = 10 ether; - - address proxyAddress; - - uint48 nonce; - - function setUp() public { - vm.createSelectFork("mainnet", 21308893); - - (clientAddress, clientPrivateKey) = makeAddrAndKey("client"); - (p2pSignerAddress, p2pSignerPrivateKey) = makeAddrAndKey("p2pSigner"); - p2pOperatorAddress = makeAddr("p2pOperator"); - nobody = makeAddr("nobody"); - - vm.startPrank(p2pOperatorAddress); - factory = new P2pEthenaProxyFactory( - p2pSignerAddress, - P2pTreasury, - sUSDe, - USDe - ); - vm.stopPrank(); - - proxyAddress = factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPoints); - } - - function test_happyPath_Mainnet() public { - deal(USDe, clientAddress, 10000e18); - - uint256 assetBalanceBefore = IERC20(USDe).balanceOf(clientAddress); - - _doDeposit(); - - uint256 assetBalanceAfter1 = IERC20(USDe).balanceOf(clientAddress); - assertEq(assetBalanceBefore - assetBalanceAfter1, DepositAmount); - - _doDeposit(); - - uint256 assetBalanceAfter2 = IERC20(USDe).balanceOf(clientAddress); - assertEq(assetBalanceAfter1 - assetBalanceAfter2, DepositAmount); - - _doDeposit(); - _doDeposit(); - - uint256 assetBalanceAfterAllDeposits = IERC20(USDe).balanceOf(clientAddress); - - _doWithdraw(10); - - uint256 assetBalanceAfterWithdraw1 = IERC20(USDe).balanceOf(clientAddress); - - assertApproxEqAbs(assetBalanceAfterWithdraw1 - assetBalanceAfterAllDeposits, DepositAmount * 4 / 10, 1); - - _doWithdraw(5); - _doWithdraw(3); - _doWithdraw(2); - _doWithdraw(1); - - uint256 assetBalanceAfterAllWithdrawals = IERC20(USDe).balanceOf(clientAddress); - - uint256 profit = 1414853635425232; - assertApproxEqAbs(assetBalanceAfterAllWithdrawals, assetBalanceBefore + profit, 1); - } - - function test_profitSplit_Mainnet() public { - deal(USDe, clientAddress, 100e18); - - uint256 clientAssetBalanceBefore = IERC20(USDe).balanceOf(clientAddress); - uint256 p2pAssetBalanceBefore = IERC20(USDe).balanceOf(P2pTreasury); - - _doDeposit(); - - uint256 shares = IERC20(sUSDe).balanceOf(proxyAddress); - uint256 assetsInEthenaBefore = IERC4626(sUSDe).convertToAssets(shares); - - _forward(10000000); - - uint256 assetsInEthenaAfter = IERC4626(sUSDe).convertToAssets(shares); - uint256 profit = assetsInEthenaAfter - assetsInEthenaBefore; - - _doWithdraw(1); - - uint256 clientAssetBalanceAfter = IERC20(USDe).balanceOf(clientAddress); - uint256 p2pAssetBalanceAfter = IERC20(USDe).balanceOf(P2pTreasury); - uint256 clientBalanceChange = clientAssetBalanceAfter - clientAssetBalanceBefore; - uint256 p2pBalanceChange = p2pAssetBalanceAfter - p2pAssetBalanceBefore; - uint256 sumOfBalanceChanges = clientBalanceChange + p2pBalanceChange; - - assertApproxEqAbs(sumOfBalanceChanges, profit, 1); - - uint256 clientBasisPointsDeFacto = clientBalanceChange * 10_000 / sumOfBalanceChanges; - uint256 p2pBasisPointsDeFacto = p2pBalanceChange * 10_000 / sumOfBalanceChanges; - - assertApproxEqAbs(ClientBasisPoints, clientBasisPointsDeFacto, 1); - assertApproxEqAbs(10_000 - ClientBasisPoints, p2pBasisPointsDeFacto, 1); - } - - function test_transferP2pSigner_Mainnet() public { - vm.startPrank(nobody); - vm.expectRevert(abi.encodeWithSelector(P2pOperator.P2pOperator__UnauthorizedAccount.selector, nobody)); - factory.transferP2pSigner(nobody); - - address oldSigner = factory.getP2pSigner(); - assertEq(oldSigner, p2pSignerAddress); - - vm.startPrank(p2pOperatorAddress); - factory.transferP2pSigner(nobody); - - address newSigner = factory.getP2pSigner(); - assertEq(newSigner, nobody); - } - - function test_setCalldataRules_Mainnet() public { - vm.startPrank(nobody); - vm.expectRevert(abi.encodeWithSelector(P2pOperator.P2pOperator__UnauthorizedAccount.selector, nobody)); - factory.setCalldataRules(address(0), bytes4(0), new P2pStructs.Rule[](0)); - - vm.startPrank(p2pOperatorAddress); - vm.expectEmit(); - emit IP2pYieldProxyFactory.P2pYieldProxyFactory__CalldataRulesSet( - address(0), - bytes4(0), - new P2pStructs.Rule[](0) - ); - factory.setCalldataRules(address(0), bytes4(0), new P2pStructs.Rule[](0)); - } - - function test_removeCalldataRules_Mainnet() public { - vm.startPrank(nobody); - vm.expectRevert(abi.encodeWithSelector(P2pOperator.P2pOperator__UnauthorizedAccount.selector, nobody)); - factory.removeCalldataRules(address(0), bytes4(0)); - - vm.startPrank(p2pOperatorAddress); - vm.expectEmit(); - emit IP2pYieldProxyFactory.P2pYieldProxyFactory__CalldataRulesRemoved( - address(0), - bytes4(0) - ); - factory.removeCalldataRules(address(0), bytes4(0)); - } - - function test_clientBasisPointsGreaterThan10000_Mainnet() public { - uint96 invalidBasisPoints = 10001; - - vm.startPrank(clientAddress); - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - invalidBasisPoints, - SigDeadline - ); - - vm.expectRevert(abi.encodeWithSelector(P2pYieldProxy__InvalidClientBasisPoints.selector, invalidBasisPoints)); - factory.deposit( - permitSingle, - permit2Signature, - invalidBasisPoints, - SigDeadline, - p2pSignerSignature - ); - } - - function test_zeroAddressAsset_Mainnet() public { - vm.startPrank(clientAddress); - - // Get the permit details - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - - // Set token to zero address - permitSingle.details.token = address(0); - - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - vm.expectRevert(P2pYieldProxy__ZeroAddressAsset.selector); - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - } - - function test_zeroAssetAmount_Mainnet() public { - vm.startPrank(clientAddress); - - // Get the permit details - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - - // Set amount to zero - permitSingle.details.amount = 0; - - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - vm.expectRevert(P2pYieldProxy__ZeroAssetAmount.selector); - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - } - - function test_depositDirectlyOnProxy_Mainnet() public { - vm.startPrank(clientAddress); - - // Add this line to give initial tokens to the client - deal(USDe, clientAddress, DepositAmount); - - // Add this line to approve tokens for Permit2 - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Get the permit details - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - - // Create proxy first via factory - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - - // Now try to call deposit directly on the proxy - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxy__NotFactoryCalled.selector, - clientAddress, - address(factory) - ) - ); - P2pEthenaProxy(proxyAddress).deposit( - permitSingle, - permit2Signature - ); - } - - function test_initializeDirectlyOnProxy_Mainnet() public { - // Create the proxy first since we need a valid proxy address to test with - proxyAddress = factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPoints); - P2pEthenaProxy proxy = P2pEthenaProxy(proxyAddress); - - vm.startPrank(clientAddress); - - // Add this line to give initial tokens to the client - deal(USDe, clientAddress, DepositAmount); - - // Add this line to approve tokens for Permit2 - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - // This will create the proxy - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - - // Now try to initialize it directly - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxy__NotFactoryCalled.selector, - clientAddress, - address(factory) - ) - ); - proxy.initialize( - clientAddress, - ClientBasisPoints - ); - vm.stopPrank(); - } - - function test_withdrawOnProxyOnlyCallableByClient_Mainnet() public { - // Create proxy and do initial deposit - deal(USDe, clientAddress, DepositAmount); - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - vm.stopPrank(); - - // Try to withdraw as non-client - vm.startPrank(nobody); - P2pEthenaProxy proxy = P2pEthenaProxy(proxyAddress); - - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxy__NotClientCalled.selector, - nobody, // _msgSender (the nobody address trying to call) - clientAddress // _actualClient (the actual client address) - ) - ); - proxy.withdrawAfterCooldown(); - vm.stopPrank(); - } - - function test_withdrawViaCallAnyFunction_Mainnet() public { - // Create proxy and do initial deposit - deal(USDe, clientAddress, DepositAmount); - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Do initial deposit - _doDeposit(); - - // Try to withdraw using callAnyFunction - P2pEthenaProxy proxy = P2pEthenaProxy(proxyAddress); - bytes memory withdrawalCallData = abi.encodeCall( - IStakedUSDe.unstake, - clientAddress - ); - - vm.startPrank(clientAddress); - - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__NoRulesDefined.selector, - USDe, - IStakedUSDe.unstake.selector - ) - ); - - proxy.callAnyFunction( - USDe, - withdrawalCallData - ); - vm.stopPrank(); - } - - function test_calldataTooShortForStartsWithRule_Mainnet() public { - // Create proxy and do initial deposit - deal(USDe, clientAddress, DepositAmount); - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Do initial deposit - _doDeposit(); - vm.stopPrank(); - - // Set rule that requires first 32 bytes to match - P2pStructs.Rule[] memory rules = new P2pStructs.Rule[](1); - rules[0] = P2pStructs.Rule({ - ruleType: P2pStructs.RuleType.StartsWith, - index: 0, - allowedBytes: new bytes(32) - }); - - vm.startPrank(p2pOperatorAddress); - factory.setCalldataRules( - sUSDe, - IERC20.balanceOf.selector, - rules - ); - vm.stopPrank(); - - // Create calldata that's too short (only 4 bytes) - bytes memory shortCalldata = abi.encodeWithSelector(IERC20.balanceOf.selector); - - vm.startPrank(clientAddress); - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__CalldataTooShortForStartsWithRule.selector, - 0, // calldata length after selector - 0, // rule index - 32 // required bytes count - ) - ); - P2pEthenaProxy(proxyAddress).callAnyFunction( - sUSDe, - shortCalldata - ); - vm.stopPrank(); - } - - function test_calldataStartsWithRuleViolated_Mainnet() public { - // Create proxy and do initial deposit - deal(USDe, clientAddress, DepositAmount); - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Do initial deposit - _doDeposit(); - vm.stopPrank(); - - // Set rule that requires first 32 bytes to match specific value - bytes memory expectedBytes = new bytes(32); - for(uint i = 0; i < 32; i++) { - expectedBytes[i] = bytes1(uint8(i)); - } - - P2pStructs.Rule[] memory rules = new P2pStructs.Rule[](1); - rules[0] = P2pStructs.Rule({ - ruleType: P2pStructs.RuleType.StartsWith, - index: 0, - allowedBytes: expectedBytes - }); - - vm.startPrank(p2pOperatorAddress); - factory.setCalldataRules( - sUSDe, - IERC20.balanceOf.selector, - rules - ); - vm.stopPrank(); - - // Create calldata with different first 32 bytes - bytes memory differentBytes = new bytes(32); - bytes memory wrongCalldata = abi.encodePacked( - IERC20.balanceOf.selector, - differentBytes - ); - - vm.startPrank(clientAddress); - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__CalldataStartsWithRuleViolated.selector, - differentBytes, - expectedBytes - ) - ); - P2pEthenaProxy(proxyAddress).callAnyFunction( - sUSDe, - wrongCalldata - ); - vm.stopPrank(); - } - - function test_calldataTooShortForEndsWithRule_Mainnet() public { - // Create proxy and do initial deposit - deal(USDe, clientAddress, DepositAmount); - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Do initial deposit - _doDeposit(); - vm.stopPrank(); - - // Set rule that requires last 32 bytes to match - P2pStructs.Rule[] memory rules = new P2pStructs.Rule[](1); - rules[0] = P2pStructs.Rule({ - ruleType: P2pStructs.RuleType.EndsWith, - index: 0, - allowedBytes: new bytes(32) - }); - - vm.startPrank(p2pOperatorAddress); - factory.setCalldataRules( - sUSDe, - IERC20.balanceOf.selector, - rules - ); - vm.stopPrank(); - - // Create calldata that's too short (only selector) - bytes memory shortCalldata = abi.encodeWithSelector(IERC20.balanceOf.selector); - - vm.startPrank(clientAddress); - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__CalldataTooShortForEndsWithRule.selector, - 0, // calldata length after selector - 32 // required bytes count - ) - ); - P2pEthenaProxy(proxyAddress).callAnyFunction( - sUSDe, - shortCalldata - ); - vm.stopPrank(); - } - - function test_calldataEndsWithRuleViolated_Mainnet() public { - // Create proxy and do initial deposit - deal(USDe, clientAddress, DepositAmount); - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Do initial deposit - _doDeposit(); - vm.stopPrank(); - - // Set rule that requires last 32 bytes to match specific value - bytes memory expectedEndBytes = new bytes(32); - for(uint i = 0; i < 32; i++) { - expectedEndBytes[i] = bytes1(uint8(i)); - } - - P2pStructs.Rule[] memory rules = new P2pStructs.Rule[](1); - rules[0] = P2pStructs.Rule({ - ruleType: P2pStructs.RuleType.EndsWith, - index: 0, - allowedBytes: expectedEndBytes - }); - - vm.startPrank(p2pOperatorAddress); - factory.setCalldataRules( - sUSDe, - IERC20.balanceOf.selector, - rules - ); - vm.stopPrank(); - - // Create calldata with different ending bytes - bytes memory wrongEndBytes = new bytes(32); - for(uint i = 0; i < 32; i++) { - wrongEndBytes[i] = bytes1(uint8(100 + i)); - } - bytes memory wrongCalldata = abi.encodePacked( - IERC20.balanceOf.selector, - wrongEndBytes - ); - - vm.startPrank(clientAddress); - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__CalldataEndsWithRuleViolated.selector, - wrongEndBytes, - expectedEndBytes - ) - ); - P2pEthenaProxy(proxyAddress).callAnyFunction( - sUSDe, - wrongCalldata - ); - vm.stopPrank(); - } - - function test_callBalanceOfViaCallAnyFunction_Mainnet() public { - // Create proxy and do initial deposit - deal(USDe, clientAddress, DepositAmount); - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Do initial deposit - _doDeposit(); - vm.stopPrank(); - - bytes memory balanceOfCalldata = abi.encodeWithSelector( - IERC20.balanceOf.selector, - proxyAddress - ); - - vm.startPrank(clientAddress); - - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__NoRulesDefined.selector, - sUSDe, - IERC20.balanceOf.selector - ) - ); - P2pEthenaProxy(proxyAddress).callAnyFunction( - sUSDe, - balanceOfCalldata - ); - vm.stopPrank(); - - P2pStructs.Rule[] memory rules = new P2pStructs.Rule[](1); - rules[0] = P2pStructs.Rule({ - ruleType: P2pStructs.RuleType.AnyCalldata, - index: 0, - allowedBytes: "" - }); - - vm.startPrank(p2pOperatorAddress); - factory.setCalldataRules( - sUSDe, - IERC20.balanceOf.selector, - rules - ); - vm.stopPrank(); - - // Call balanceOf via callAnyFunction - vm.startPrank(clientAddress); - P2pEthenaProxy proxy = P2pEthenaProxy(proxyAddress); - proxy.callAnyFunction( - sUSDe, - balanceOfCalldata - ); - vm.stopPrank(); - } - - function test_getP2pLendingProxyFactory__NoRulesDefined_Mainnet() public { - // Create proxy first via factory - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - - // Add this line to give tokens to the client before attempting deposit - deal(USDe, clientAddress, DepositAmount); - - vm.startPrank(clientAddress); - - // Add this line to approve tokens for Permit2 - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - - // Try to call a function with no rules defined - bytes memory someCalldata = abi.encodeWithSelector( - IERC20.transfer.selector, - address(0), - 0 - ); - - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__NoRulesDefined.selector, - USDe, - IERC20.transfer.selector - ) - ); - - P2pEthenaProxy(proxyAddress).callAnyFunction( - USDe, - someCalldata - ); - - vm.stopPrank(); - } - - function test_getP2pLendingProxyFactory__ZeroP2pSignerAddress_Mainnet() public { - vm.startPrank(p2pOperatorAddress); - vm.expectRevert(P2pYieldProxyFactory__ZeroP2pSignerAddress.selector); - factory.transferP2pSigner(address(0)); - vm.stopPrank(); - } - - function test_getHashForP2pSigner_Mainnet() public view { - bytes32 expectedHash = keccak256(abi.encode( - clientAddress, - ClientBasisPoints, - SigDeadline, - address(factory), - block.chainid - )); - - bytes32 actualHash = factory.getHashForP2pSigner( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - assertEq(actualHash, expectedHash); - } - - function test_getPermit2HashTypedData_Mainnet() public view { - // Create a permit single struct - IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ - details: IAllowanceTransfer.PermitDetails({ - token: USDe, - amount: uint160(DepositAmount), - expiration: uint48(SigDeadline), - nonce: 0 - }), - spender: proxyAddress, - sigDeadline: SigDeadline - }); - - // Get the permit hash - bytes32 permitHash = factory.getPermitHash(permitSingle); - - // Get the typed data hash - bytes32 actualTypedDataHash = factory.getPermit2HashTypedData(permitHash); - - // Calculate expected hash - bytes32 expectedTypedDataHash = keccak256( - abi.encodePacked( - "\x19\x01", - Permit2Lib.PERMIT2.DOMAIN_SEPARATOR(), - permitHash - ) - ); - - assertEq(actualTypedDataHash, expectedTypedDataHash); - - // Test the overloaded function that takes PermitSingle directly - bytes32 actualTypedDataHashFromPermitSingle = factory.getPermit2HashTypedData(permitSingle); - assertEq(actualTypedDataHashFromPermitSingle, expectedTypedDataHash); - } - - function test_supportsInterface_Mainnet() public view { - // Test IP2pLendingProxyFactory interface support - bool supportsP2pLendingProxyFactory = factory.supportsInterface(type(IP2pYieldProxyFactory).interfaceId); - assertTrue(supportsP2pLendingProxyFactory); - - // Test IERC165 interface support - bool supportsERC165 = factory.supportsInterface(type(IERC165).interfaceId); - assertTrue(supportsERC165); - - // Test non-supported interface - bytes4 nonSupportedInterfaceId = bytes4(keccak256("nonSupportedInterface()")); - bool supportsNonSupported = factory.supportsInterface(nonSupportedInterfaceId); - assertFalse(supportsNonSupported); - } - - function test_p2pSignerSignatureExpired_Mainnet() public { - // Add this line to give tokens to the client before attempting deposit - deal(USDe, clientAddress, DepositAmount); - - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - - // Get p2p signer signature with expired deadline - uint256 expiredDeadline = block.timestamp - 1; - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - expiredDeadline - ); - - vm.expectRevert( - abi.encodeWithSelector( - P2pYieldProxyFactory__P2pSignerSignatureExpired.selector, - expiredDeadline - ) - ); - - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - expiredDeadline, - p2pSignerSignature - ); - vm.stopPrank(); - } - - function test_invalidP2pSignerSignature_Mainnet() public { - // Add this line to give tokens to the client before attempting deposit - deal(USDe, clientAddress, DepositAmount); - - vm.startPrank(clientAddress); - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - - // Create an invalid signature by using a different private key - uint256 wrongPrivateKey = 0x12345; // Some random private key - bytes32 messageHash = ECDSA.toEthSignedMessageHash( - factory.getHashForP2pSigner( - clientAddress, - ClientBasisPoints, - SigDeadline - ) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, messageHash); - bytes memory invalidSignature = abi.encodePacked(r, s, v); - - vm.expectRevert(P2pYieldProxyFactory__InvalidP2pSignerSignature.selector); - - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - invalidSignature - ); - vm.stopPrank(); - } - - function test_viewFunctions_Mainnet() public { - // Add this line to give tokens to the client before attempting deposit - deal(USDe, clientAddress, DepositAmount); - - vm.startPrank(clientAddress); - - // Add this line to approve tokens for Permit2 - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - - // Create proxy first via factory - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - IAllowanceTransfer.PermitSingle memory permitSingle = _getPermitSingleForP2pYieldProxy(); - bytes memory permit2Signature = _getPermit2SignatureForP2pYieldProxy(permitSingle); - - factory.deposit( - permitSingle, - permit2Signature, - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - - P2pEthenaProxy proxy = P2pEthenaProxy(proxyAddress); - assertEq(proxy.getFactory(), address(factory)); - assertEq(proxy.getP2pTreasury(), P2pTreasury); - assertEq(proxy.getClient(), clientAddress); - assertEq(proxy.getClientBasisPoints(), ClientBasisPoints); - assertEq(proxy.getTotalDeposited(USDe), DepositAmount); - assertEq(factory.getP2pSigner(), p2pSignerAddress); - assertEq(factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPoints), proxyAddress); - } - - function test_acceptP2pOperator_Mainnet() public { - // Initial state check - assertEq(factory.getP2pOperator(), p2pOperatorAddress); - - // Only operator can initiate transfer - vm.startPrank(nobody); - vm.expectRevert( - abi.encodeWithSelector( - P2pOperator.P2pOperator__UnauthorizedAccount.selector, - nobody - ) - ); - factory.transferP2pOperator(nobody); - vm.stopPrank(); - - // Operator initiates transfer - address newOperator = makeAddr("newOperator"); - vm.startPrank(p2pOperatorAddress); - factory.transferP2pOperator(newOperator); - - // Check pending operator is set - assertEq(factory.getPendingP2pOperator(), newOperator); - // Check current operator hasn't changed yet - assertEq(factory.getP2pOperator(), p2pOperatorAddress); - vm.stopPrank(); - - // Wrong address cannot accept transfer - vm.startPrank(nobody); - vm.expectRevert( - abi.encodeWithSelector( - P2pOperator.P2pOperator__UnauthorizedAccount.selector, - nobody - ) - ); - factory.acceptP2pOperator(); - vm.stopPrank(); - - // New operator accepts transfer - vm.startPrank(newOperator); - factory.acceptP2pOperator(); - - // Check operator was updated - assertEq(factory.getP2pOperator(), newOperator); - // Check pending operator was cleared - assertEq(factory.getPendingP2pOperator(), address(0)); - vm.stopPrank(); - - // Old operator can no longer call operator functions - vm.startPrank(p2pOperatorAddress); - vm.expectRevert( - abi.encodeWithSelector( - P2pOperator.P2pOperator__UnauthorizedAccount.selector, - p2pOperatorAddress - ) - ); - factory.transferP2pOperator(p2pOperatorAddress); - vm.stopPrank(); - } - - function _getPermitSingleForP2pYieldProxy() private returns(IAllowanceTransfer.PermitSingle memory) { - IAllowanceTransfer.PermitDetails memory permitDetails = IAllowanceTransfer.PermitDetails({ - token: USDe, - amount: uint160(DepositAmount), - expiration: uint48(SigDeadline), - nonce: nonce - }); - nonce++; - - // data for factory - IAllowanceTransfer.PermitSingle memory permitSingleForP2pYieldProxy = IAllowanceTransfer.PermitSingle({ - details: permitDetails, - spender: proxyAddress, - sigDeadline: SigDeadline - }); - - return permitSingleForP2pYieldProxy; - } - - function _getPermit2SignatureForP2pYieldProxy(IAllowanceTransfer.PermitSingle memory permitSingleForP2pYieldProxy) private view returns(bytes memory) { - bytes32 permitSingleForP2pYieldProxyHash = factory.getPermit2HashTypedData(PermitHash.hash(permitSingleForP2pYieldProxy)); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(clientPrivateKey, permitSingleForP2pYieldProxyHash); - bytes memory permit2SignatureForP2pYieldProxy = abi.encodePacked(r1, s1, v1); - return permit2SignatureForP2pYieldProxy; - } - - function _getP2pSignerSignature( - address _clientAddress, - uint96 _clientBasisPoints, - uint256 _sigDeadline - ) private view returns(bytes memory) { - // p2p signer signing - bytes32 hashForP2pSigner = factory.getHashForP2pSigner( - _clientAddress, - _clientBasisPoints, - _sigDeadline - ); - bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(p2pSignerPrivateKey, ethSignedMessageHashForP2pSigner); - bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); - return p2pSignerSignature; - } - - function _doDeposit() private { - IAllowanceTransfer.PermitSingle memory permitSingleForP2pYieldProxy = _getPermitSingleForP2pYieldProxy(); - bytes memory permit2SignatureForP2pYieldProxy = _getPermit2SignatureForP2pYieldProxy(permitSingleForP2pYieldProxy); - bytes memory p2pSignerSignature = _getP2pSignerSignature( - clientAddress, - ClientBasisPoints, - SigDeadline - ); - - vm.startPrank(clientAddress); - if (IERC20(USDe).allowance(clientAddress, address(Permit2Lib.PERMIT2)) == 0) { - IERC20(USDe).safeApprove(address(Permit2Lib.PERMIT2), type(uint256).max); - } - factory.deposit( - permitSingleForP2pYieldProxy, - permit2SignatureForP2pYieldProxy, - - ClientBasisPoints, - SigDeadline, - p2pSignerSignature - ); - vm.stopPrank(); - } - - function _doWithdraw(uint256 denominator) private { - uint256 sharesBalance = IERC20(sUSDe).balanceOf(proxyAddress); - console.log("sharesBalance"); - console.log(sharesBalance); - - uint256 sharesToWithdraw = sharesBalance / denominator; - - vm.startPrank(clientAddress); - P2pEthenaProxy(proxyAddress).cooldownShares(sharesToWithdraw); - - _forward(10_000 * 7); - - P2pEthenaProxy(proxyAddress).withdrawAfterCooldown(); - vm.stopPrank(); - } - - /// @dev Rolls & warps the given number of blocks forward the blockchain. - function _forward(uint256 blocks) internal { - vm.roll(block.number + blocks); - vm.warp(block.timestamp + blocks * 13); - } -} \ No newline at end of file diff --git a/test/OptimismNative.t.sol b/test/OptimismNative.t.sol new file mode 100644 index 0000000..69567af --- /dev/null +++ b/test/OptimismNative.t.sol @@ -0,0 +1,485 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../src/@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../src/access/P2pOperator.sol"; +import "../src/access/P2pOperator.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import "../src/common/AllowedCalldataChecker.sol"; +import "../src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol"; +import "./mocks/MockAllowedCalldataChecker.sol"; +import "./utils/Error.sol"; +import "./utils/merkle/helper/MerkleReader.sol"; +import "forge-std/Test.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import "forge-std/console2.sol"; + +contract OptimismNative is Test, MerkleReader { + using SafeERC20 for IERC20; + + address constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant RewardsDistributorInstance = 0xce23bD7205bF2B543F6B4eeC00Add0C111FEFc3B; + + address constant RewardsDistributorAdmin = 0xf82F3D7Df94FC2994315c32322DA6238cA2A2f7f; + + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + + uint256 constant SuperformId = 62771017356379199835532377802369906037722899472923496568460; + + bytes constant LiqRequestTxSata = + hex"4630a0d8dac814cc41f28f3f61b1c75cf080011e2f868b0037392f8ae14f7b42bae3be4a00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000b8138fff124dd7f91abf412b73be453fb140568c0000000000000000000000000000000000000000000000000022bd6ab3cf83760000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30783030303030303030303030303030303030303030303030303030303030303030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b50000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b500000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb000000000000000000000000000000000000000000000000002bd72a2487400000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000664e21fd0e90000000000000000000000000000000000000000000000000000000000000020000000000000000000000000c7d3ab410d49b664d03fe5b1038852ac852b1b29000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000f3010100000048000000ba12222222228d8ba445958a75a0704d566bf2c87ca75bdea9dede97f8b13c6641b768650cb837820002000000000000000000d5000000000000000000002bd72a248740000beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee1f32b1c2345538c0c6f582fcb022739c4a194ebb1231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000680a01200000005400000000000000000000000000000000000000000000000000000000000000000000000000000000000000026583c34c00000000000000000024917dcabf7ce84f82e73edb06d29ff62c91ec8f5ff06571bdeb2900000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000002bd72a248740000000000000000000000000000000000000000000000000000022bd6ab3cf8376000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025d7b22536f75726365223a226c692e6669222c22416d6f756e74496e555344223a2232312e3339343431353230343638313437222c22416d6f756e744f7574555344223a2232312e333934363835343533303031343635222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a223130323933303638363230303730313230222c2254696d657374616d70223a313734353438343931322c22526f7574654944223a2238626338353330632d376465312d343437302d393631622d363734666431616662663337222c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a2258424a3749307656656b7368513459435953343971574345494e53793755696f304e6249706d556f574945506c743335616b55734c394a3333777857506956794d37685357545470642b62627a56574a394d67694a32346a4e43324861767463766a4a77716145554b723757474b5448387835747134304743362f7478374735594635487a664a49342f66456c353657474a39327635304d664d4b31527366516d326a43444a7849656d376e71484a4644684f43686b6e67394433454d6f6b686d3030696675637a6158574f444d722f346464396e656e58336869364847775a51412b754667557a77387331526373646570725a3557656958705033572b41596a6a394b5a794a4a627346424768524c6768516f3238486843314c3373412f6842346e347a59374669566d6967534a614b4c325475734535677450484935684d6d66764d2f4876437858797a4c413359666b734678413d3d227d7d00000000000000000000000000000000000000000000000000000000000000"; + + P2pSuperformProxyFactory private factory; + + address private clientAddress; + uint256 private clientPrivateKey; + + address private p2pSignerAddress; + uint256 private p2pSignerPrivateKey; + + address private p2pOperatorAddress; + address private nobody; + + uint256 constant SigDeadline = 1789558996; + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10_000; // 0% fee + uint256 constant DepositAmount = 12340000000000000; + uint256 constant VaultAmount = 10293068620070120; + uint256 constant VaultOutputAmount = 10284585609330856; + + address proxyAddress; + + uint48 nonce; + + uint64 public constant CHAIN_ID = 10; + + uint256 totalUSDCToDeposit; + uint256 totalDAIToDeposit; + + ProxyAdmin private admin; + TransparentUpgradeableProxy private tup; + + function setUp() public { + vm.createSelectFork("optimism", 134943023); + + (clientAddress, clientPrivateKey) = makeAddrAndKey("client"); + (p2pSignerAddress, p2pSignerPrivateKey) = makeAddrAndKey("p2pSigner"); + p2pOperatorAddress = makeAddr("p2pOperator"); + nobody = makeAddr("nobody"); + + vm.startPrank(p2pOperatorAddress); + AllowedCalldataChecker implementation = new AllowedCalldataChecker(); + admin = new ProxyAdmin(); + bytes memory initData = abi.encodeWithSelector(AllowedCalldataChecker.initialize.selector); + tup = new TransparentUpgradeableProxy(address(implementation), address(admin), initData); + factory = new P2pSuperformProxyFactory( + p2pSignerAddress, + p2pOperatorAddress, + P2pTreasury, + SuperformRouter, + SuperPositions, + address(tup), + RewardsDistributorInstance + ); + vm.stopPrank(); + + proxyAddress = + factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + deal(clientAddress, 10000e18); + } + + function test_happyPath_native_Optimism() public { + _doDeposit(); + _doWithdraw(); + } + + function test_P2pYieldProxy__EmergencyTokenWithdraw() public { + _doDeposit(); + + // Deal USDT to the proxy + uint256 ethAmount = 1000 ether; // 1000 ETH + deal(proxyAddress, ethAmount); + + // Verify initial balances + assertEq(proxyAddress.balance, ethAmount); + uint256 clientInitialBalance = clientAddress.balance; + + vm.expectRevert(abi.encodeWithSelector(P2pYieldProxy__NotClientCalled.selector, address(this), clientAddress)); + P2pYieldProxy(payable(proxyAddress)).emergencyNativeWithdraw(); + + // Call emergencyTokenWithdraw as client + vm.startPrank(clientAddress); + P2pYieldProxy(payable(proxyAddress)).emergencyNativeWithdraw(); + vm.stopPrank(); + + // Verify final balances + assertEq(proxyAddress.balance, 0); + assertEq(clientAddress.balance - clientInitialBalance, ethAmount); + } + + function test_P2pSuperformProxy__NativeAmountToDepositAfterFeeLessThanliqRequestNativeAmount() public { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + + LiqRequest memory liqRequest = LiqRequest({ + txData: LiqRequestTxSata, + token: NATIVE, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: DepositAmount + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: VaultAmount, + outputAmount: VaultOutputAmount, + maxSlippage: 500, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + uint256 actual = DepositAmount - 1; + vm.expectRevert( + abi.encodeWithSelector( + P2pSuperformProxy__NativeAmountToDepositAfterFeeLessThanliqRequestNativeAmount.selector, + actual, + DepositAmount + ) + ); + factory.deposit{value: actual}( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function testAllowedCalldataChecker__NoAllowedCalldata() public { + _doDeposit(); + + address yieldProtocolAddress = makeAddr("yieldProtocolAddress"); + bytes memory yieldProtocolCalldata = new bytes(42); + + vm.startPrank(clientAddress); + vm.expectRevert(AllowedCalldataChecker__NoAllowedCalldata.selector); + IP2pSuperformProxy(proxyAddress).callAnyFunction(yieldProtocolAddress, yieldProtocolCalldata); + vm.stopPrank(); + } + + function testP2pSuperformProxy_GetClientAddress() public { + _doDeposit(); + + assertEq(IP2pSuperformProxy(proxyAddress).getClient(), clientAddress); + } + + function testP2pSuperformProxy_GetP2pTreasuryAddress() public { + _doDeposit(); + + assertEq(IP2pSuperformProxy(proxyAddress).getP2pTreasury(), P2pTreasury); + } + + function testP2pSuperformProxy_GetClientBasisPointsOfDeposit() public { + _doDeposit(); + + assertEq(IP2pSuperformProxy(proxyAddress).getClientBasisPointsOfDeposit(), ClientBasisPointsOfDeposit); + } + + function testP2pSuperformProxy_GetClientBasisPointsOfProfit() public { + _doDeposit(); + + assertEq(IP2pSuperformProxy(proxyAddress).getClientBasisPointsOfProfit(), ClientBasisPointsOfProfit); + } + + function testP2pSuperformProxyFactory_GetP2pOperatorAddress() public view { + assertEq(factory.getP2pOperator(), p2pOperatorAddress); + } + + function testP2pSuperformProxyFactory_GetP2pSignerAddress() public view { + assertEq(factory.getP2pSigner(), p2pSignerAddress); + } + + function testP2pSuperformProxyFactory_GetClientToProxy() public { + _doDeposit(); + + address proxy = + factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + assertEq(proxy, proxyAddress); + } + + function testAllowedCalldataCheckerUpgrade() public { + _doDeposit(); + + address yieldProtocolAddress = makeAddr("yieldProtocolAddress"); + bytes memory yieldProtocolCalldata = new bytes(42); + + vm.startPrank(clientAddress); + vm.expectRevert(AllowedCalldataChecker__NoAllowedCalldata.selector); + IP2pSuperformProxy(proxyAddress).callAnyFunction(yieldProtocolAddress, yieldProtocolCalldata); + vm.stopPrank(); + + vm.startPrank(p2pOperatorAddress); + MockAllowedCalldataChecker newImplementation = new MockAllowedCalldataChecker(); + admin.upgrade(ITransparentUpgradeableProxy(address(tup)), address(newImplementation)); + vm.stopPrank(); + + vm.startPrank(clientAddress); + vm.expectRevert("Address: call to non-contract"); + IP2pSuperformProxy(proxyAddress).callAnyFunction(yieldProtocolAddress, yieldProtocolCalldata); + vm.stopPrank(); + } + + function testP2pSuperformProxy_CheckClaim_UnauthorizedAccount() public { + _doDeposit(); + + address unauthorizedClaimer = makeAddr("unauthorizedClaimer"); + uint256[] memory periodIds = new uint256[](1); + periodIds[0] = 1; + + address[][] memory rewardTokens = new address[][](1); + rewardTokens[0] = new address[](1); + rewardTokens[0][0] = makeAddr("rewardToken"); + + uint256[][] memory amountsClaimed = new uint256[][](1); + amountsClaimed[0] = new uint256[](1); + amountsClaimed[0][0] = 100; + + bytes32[][] memory proofs = new bytes32[][](1); + proofs[0] = new bytes32[](1); + proofs[0][0] = bytes32(0); + + vm.startPrank(unauthorizedClaimer); + vm.expectRevert( + abi.encodeWithSelector(P2pOperator.P2pOperator__UnauthorizedAccount.selector, unauthorizedClaimer) + ); + IP2pSuperformProxy(proxyAddress).batchClaim(periodIds, rewardTokens, amountsClaimed, proofs); + vm.stopPrank(); + } + + function testP2pSuperformProxy__ReceiverAddressShouldBeP2pSuperformProxy() public { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + + LiqRequest memory liqRequest = LiqRequest({ + txData: LiqRequestTxSata, + token: NATIVE, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: DepositAmount + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: VaultAmount, + outputAmount: VaultOutputAmount, + maxSlippage: 500, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: address(0x123), // Setting to a different address than proxyAddress + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.expectRevert( + abi.encodeWithSelector(P2pSuperformProxy__ReceiverAddressShouldBeP2pSuperformProxy.selector, address(0x123)) + ); + factory.deposit{value: DepositAmount}( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function testP2pSuperformProxy__ReceiverAddressSPShouldBeP2pSuperformProxy() public { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + + LiqRequest memory liqRequest = LiqRequest({ + txData: LiqRequestTxSata, + token: NATIVE, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: DepositAmount + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: VaultAmount, + outputAmount: VaultOutputAmount, + maxSlippage: 500, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: address(0x123), // Setting to a different address than proxyAddress + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.expectRevert( + abi.encodeWithSelector( + P2pSuperformProxy__ReceiverAddressSPShouldBeP2pSuperformProxy.selector, address(0x123) + ) + ); + factory.deposit{value: DepositAmount}( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function testP2pSuperformProxy__ShouldNotRetain4626() public { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + + LiqRequest memory liqRequest = LiqRequest({ + txData: LiqRequestTxSata, + token: NATIVE, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: DepositAmount + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: VaultAmount, + outputAmount: VaultOutputAmount, + maxSlippage: 500, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: true, // Setting retain4626 to true + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + vm.expectRevert(P2pSuperformProxy__ShouldNotRetain4626.selector); + factory.deposit{value: DepositAmount}( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function _getVaultAddress() private pure returns (address) { + return address(uint160(SuperformId)); + } + + function _getP2pSignerSignature( + address _clientAddress, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, + uint256 _sigDeadline + ) private view returns (bytes memory) { + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + _clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _sigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(p2pSignerPrivateKey, ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + return p2pSignerSignature; + } + + function _doDeposit() private { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + + LiqRequest memory liqRequest = LiqRequest({ + txData: LiqRequestTxSata, + token: NATIVE, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: DepositAmount + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: VaultAmount, + outputAmount: VaultOutputAmount, + maxSlippage: 500, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + factory.deposit{value: DepositAmount * 113 / 100}( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function _doWithdraw() private { + LiqRequest memory liqRequest = LiqRequest({ + txData: hex"4630a0d8043bf297c37e0f5ca30079351a85da894514103f8da67224f7bb0b337a1ff61300000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000fd35454f266dc9f672985260029f1686c6b6036c000000000000000000000000000000000000000000000000000227962180fd4f0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a3078303030303030303030303030303030303030303030303030303030303030303030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000060e8c01e8e39b10202e39e62001f08092cc03ca000000000000000000000000060e8c01e8e39b10202e39e62001f08092cc03ca0000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000039843d934a78a00000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001642646478b0000000000000000000000001f32b1c2345538c0c6f582fcb022739c4a194ebb00000000000000000000000000000000000000000000000000039843d934a78a000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000227962180fd4f0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000070021f32b1c2345538c0c6f582fcb022739c4a194ebb01ffff01bf30ff33cf9c6b0c48702ff17891293b002dfea401060e8c01e8e39b10202e39e62001f08092cc03ca01420000000000000000000000000000000000000601ffff02001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + token: NATIVE, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: 1011008197038423, + outputAmount: 1011842104469386, + maxSlippage: 5000, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultWithdraw, (req)); + vm.startPrank(clientAddress); + P2pSuperformProxy(payable(proxyAddress)).withdraw(superformCalldata); + vm.stopPrank(); + } + + /// @dev Rolls & warps the given number of blocks forward the blockchain. + function _forward(uint256 blocks) internal { + vm.roll(block.number + blocks); + vm.warp(block.timestamp + blocks * 13); + } +} diff --git a/test/OptimismUSDT.t.sol b/test/OptimismUSDT.t.sol new file mode 100644 index 0000000..7abb295 --- /dev/null +++ b/test/OptimismUSDT.t.sol @@ -0,0 +1,435 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../src/@openzeppelin/contracts/interfaces/IERC4626.sol"; +import "../src/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "../src/@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../src/adapters/superform/IERC1155A.sol"; +import "../src/access/P2pOperator.sol"; +import "../src/adapters/superform/p2pSuperformProxyFactory/P2pSuperformProxyFactory.sol"; +import "../src/adapters/superform/p2pSuperformProxy/P2pSuperformProxy.sol"; +import "../src/adapters/superform/p2pSuperformProxy/IP2pSuperformProxy.sol"; +import "../src/p2pYieldProxy/IP2pYieldProxy.sol"; +import "../src/common/AllowedCalldataChecker.sol"; +import "../src/p2pYieldProxyFactory/P2pYieldProxyFactory.sol"; +import "./utils/merkle/helper/MerkleReader.sol"; +import "forge-std/Test.sol"; +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; +import "forge-std/console2.sol"; + +contract OptimismUSDT is Test, MerkleReader { + using SafeERC20 for IERC20; + + address constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address constant USDT = 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; + address constant SuperformRouter = 0xa195608C2306A26f727d5199D5A382a4508308DA; + address constant SuperPositions = 0x01dF6fb6a28a89d6bFa53b2b3F20644AbF417678; + address constant RewardsDistributorInstance = 0xce23bD7205bF2B543F6B4eeC00Add0C111FEFc3B; + + address constant RewardsDistributorAdmin = 0xf82F3D7Df94FC2994315c32322DA6238cA2A2f7f; + + address constant P2pTreasury = 0x641ca805C75cC5D1ffa78C0181Aba1F77BD17904; + + uint256 constant SuperformId = 62771017356190754913478451444852273738203985736479809223259; + + P2pSuperformProxyFactory private factory; + + address private clientAddress; + uint256 private clientPrivateKey; + + address private p2pSignerAddress; + uint256 private p2pSignerPrivateKey; + + address private p2pOperatorAddress; + address private nobody; + + uint256 constant SigDeadline = 1789558996; + uint48 constant ClientBasisPointsOfProfit = 8700; // 13% fee + uint48 constant ClientBasisPointsOfDeposit = 10_000; // 0% fee + uint256 constant DepositAmount = 124071208818789728; + uint256 constant SharesAmount = 124071208818789728; + + uint256 DepositAmountWithFee; + + address proxyAddress; + + uint64 public constant CHAIN_ID = 10; + + uint256 totalUSDCToDeposit; + uint256 totalDAIToDeposit; + + function setUp() public { + vm.createSelectFork("optimism", 133700000); + + (clientAddress, clientPrivateKey) = makeAddrAndKey("client"); + (p2pSignerAddress, p2pSignerPrivateKey) = makeAddrAndKey("p2pSigner"); + p2pOperatorAddress = makeAddr("p2pOperator"); + nobody = makeAddr("nobody"); + + vm.startPrank(p2pOperatorAddress); + AllowedCalldataChecker implementation = new AllowedCalldataChecker(); + ProxyAdmin admin = new ProxyAdmin(); + bytes memory initData = abi.encodeWithSelector(AllowedCalldataChecker.initialize.selector); + TransparentUpgradeableProxy tup = + new TransparentUpgradeableProxy(address(implementation), address(admin), initData); + factory = new P2pSuperformProxyFactory( + p2pSignerAddress, + p2pOperatorAddress, + P2pTreasury, + SuperformRouter, + SuperPositions, + address(tup), + RewardsDistributorInstance + ); + vm.stopPrank(); + + proxyAddress = + factory.predictP2pYieldProxyAddress(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit); + + deal(USDT, clientAddress, 10000e18); + + DepositAmountWithFee = DepositAmount + DepositAmount * (10_000 - ClientBasisPointsOfDeposit) / 10_000; + } + + function test_happyPath_Optimism() public { + _doDeposit(); + _doWithdraw(); + } + + function test_P2pOperator2Step() public { + // Get initial P2pOperator + address initialP2pOperator = factory.getP2pOperator(); + assertEq(initialP2pOperator, p2pOperatorAddress); + + // Create new P2pOperator address + address newP2pOperator = makeAddr("newP2pOperator"); + + // Step 1: Current P2pOperator initiates transfer + vm.prank(p2pOperatorAddress); + factory.transferP2pOperator(newP2pOperator); + + // Verify pending P2pOperator is set + assertEq(factory.getPendingP2pOperator(), newP2pOperator); + // Verify current P2pOperator hasn't changed yet + assertEq(factory.getP2pOperator(), p2pOperatorAddress); + + // Step 2: New P2pOperator accepts transfer + vm.prank(newP2pOperator); + factory.acceptP2pOperator(); + + // Verify P2pOperator was updated + assertEq(factory.getP2pOperator(), newP2pOperator); + // Verify pending P2pOperator was cleared + assertEq(factory.getPendingP2pOperator(), address(0)); + } + + function test_P2pYieldProxyFactory__InvalidP2pSignerSignature() public { + bytes memory p2pSignerSignature; + bytes memory superformCalldata = new bytes(3); // Less than 4 bytes for function selector + + vm.startPrank(clientAddress); + vm.expectRevert(P2pYieldProxyFactory__InvalidP2pSignerSignature.selector); + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function test_P2pYieldProxy__EmergencyTokenWithdraw() public { + _doDeposit(); + + // Deal USDT to the proxy + uint256 usdtAmount = 1000 * 10 ** 6; // 1000 USDT with 6 decimals + deal(USDT, proxyAddress, usdtAmount); + + // Verify initial balances + assertEq(IERC20(USDT).balanceOf(proxyAddress), usdtAmount); + uint256 clientInitialBalance = IERC20(USDT).balanceOf(clientAddress); + + vm.expectRevert(abi.encodeWithSelector(P2pYieldProxy__NotClientCalled.selector, address(this), clientAddress)); + P2pYieldProxy(payable(proxyAddress)).emergencyTokenWithdraw(USDT); + + // Call emergencyTokenWithdraw as client + vm.startPrank(clientAddress); + P2pYieldProxy(payable(proxyAddress)).emergencyTokenWithdraw(USDT); + vm.stopPrank(); + + // Verify final balances + assertEq(IERC20(USDT).balanceOf(proxyAddress), 0); + assertEq(IERC20(USDT).balanceOf(clientAddress) - clientInitialBalance, usdtAmount); + } + + function test_P2pSuperformProxy__SuperformCalldataTooShort() public { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + bytes memory superformCalldata = new bytes(3); // Less than 4 bytes for function selector + + vm.startPrank(clientAddress); + vm.expectRevert(P2pSuperformProxy__SuperformCalldataTooShort.selector); + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function test_P2pSuperformProxy__SelectorNotSupported() public { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + // Create calldata with an unsupported selector + bytes4 unsupportedSelector = bytes4(keccak256("unsupportedFunction()")); + bytes memory superformCalldata = abi.encodePacked(unsupportedSelector, "42"); + + vm.startPrank(clientAddress); + vm.expectRevert(abi.encodeWithSelector(P2pSuperformProxy__SelectorNotSupported.selector, unsupportedSelector)); + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function test_calculateAccruedRewards_AfterDeposit() public { + _doDeposit(); + + // After deposit, check that rewards calculation works + // The exact value depends on vault implementation, but it should be calculable + P2pSuperformProxy(payable(proxyAddress)).calculateAccruedRewards(SuperformId, USDT); + } + + function test_calculateAccruedRewards_Positive() public { + _doDeposit(); + + // Get current shares + uint256 shares = IERC1155A(SuperPositions).balanceOf(proxyAddress, SuperformId); + assertGt(shares, 0, "Should have shares after deposit"); + + // Test that the function works with existing shares + P2pSuperformProxy(payable(proxyAddress)).calculateAccruedRewards(SuperformId, USDT); + } + + function test_calculateAccruedRewards_AfterPartialWithdraw() public { + _doDeposit(); + + // Do a partial withdrawal + _doWithdraw(); // Withdraw all shares + + // Check that rewards calculation still works + P2pSuperformProxy(payable(proxyAddress)).calculateAccruedRewards(SuperformId, USDT); + } + + function test_calculateAccruedRewards_DifferentAssets() public { + _doDeposit(); + + // Test with different assets + int256 usdtRewards = P2pSuperformProxy(payable(proxyAddress)).calculateAccruedRewards(SuperformId, USDT); + int256 nativeRewards = P2pSuperformProxy(payable(proxyAddress)).calculateAccruedRewards(SuperformId, NATIVE); + + // Both should work (though native might be 0 if no native deposits) + usdtRewards; // Just ensure it doesn't revert + nativeRewards; // Just ensure it doesn't revert + } + + function test_batchclaim_proxy() public { + _doDeposit(); + + _addRoot(); + _addRoot24(); + + // common user + address user = proxyAddress; + + uint256[] memory periodIds = new uint256[](2); + periodIds[0] = 23; + periodIds[1] = 24; + + bytes32[][] memory proofs = new bytes32[][](2); + + address[][] memory tokensToClaim = new address[][](2); + + uint256[][] memory amountsToClaim = new uint256[][](2); + for (uint256 periodId = 0; periodId < 2; periodId++) { + (,,,, bytes32[] memory proof_, address[] memory tokensToClaim_, uint256[] memory amountsToClaim_) = + _generateMerkleTree(MerkleReader.MerkleArgs(periodId + 23, user, CHAIN_ID)); + + proofs[periodId] = proof_; + tokensToClaim[periodId] = tokensToClaim_; + amountsToClaim[periodId] = amountsToClaim_; + } + + vm.prank(clientAddress); + IP2pSuperformProxy(payable(proxyAddress)).batchClaim(periodIds, tokensToClaim, amountsToClaim, proofs); + } + + function test_batchclaim_randomClaimer_claimAndAlreadyClaimed() public { + _addRoot(); + _addRoot24(); + + // common user + address user = proxyAddress; + + uint256[] memory periodIds = new uint256[](2); + periodIds[0] = 23; + periodIds[1] = 24; + + bytes32[][] memory proofs = new bytes32[][](2); + + address[][] memory tokensToClaim = new address[][](2); + + uint256[][] memory amountsToClaim = new uint256[][](2); + for (uint256 periodId = 0; periodId < 2; periodId++) { + (,,,, bytes32[] memory proof_, address[] memory tokensToClaim_, uint256[] memory amountsToClaim_) = + _generateMerkleTree(MerkleReader.MerkleArgs(periodId + 23, user, CHAIN_ID)); + + proofs[periodId] = proof_; + tokensToClaim[periodId] = tokensToClaim_; + amountsToClaim[periodId] = amountsToClaim_; + } + + /// @dev tests a claim initiated by a random user on behalf of user + vm.prank(address(0x777)); + IRewardsDistributor(RewardsDistributorInstance).batchClaim( + user, periodIds, tokensToClaim, amountsToClaim, proofs + ); + + vm.expectRevert(IRewardsDistributor.ALREADY_CLAIMED.selector); + vm.prank(user); + IRewardsDistributor(RewardsDistributorInstance).batchClaim( + user, periodIds, tokensToClaim, amountsToClaim, proofs + ); + } + + function _addRoot() internal { + bytes32 root; + uint256 usdcToDeposit; + uint256 daiToDeposit; + uint256 periodId = 23; // IRewardsDistributor(RewardsDistributorInstance).currentPeriodId(); + (root,, usdcToDeposit, daiToDeposit,,,) = + _generateMerkleTree(MerkleReader.MerkleArgs(periodId, proxyAddress, CHAIN_ID)); + + vm.startPrank(RewardsDistributorAdmin); + IRewardsDistributor(RewardsDistributorInstance).setPeriodicRewards(root); + totalUSDCToDeposit += usdcToDeposit; + totalDAIToDeposit += daiToDeposit; + + deal(USDC, RewardsDistributorInstance, totalUSDCToDeposit); + deal(DAI, RewardsDistributorInstance, totalDAIToDeposit); + vm.stopPrank(); + } + + function _addRoot24() internal { + bytes32 root; + uint256 usdcToDeposit; + uint256 daiToDeposit; + (root,, usdcToDeposit, daiToDeposit,,,) = + _generateMerkleTree(MerkleReader.MerkleArgs(24, proxyAddress, CHAIN_ID)); + + vm.startPrank(RewardsDistributorAdmin); + IRewardsDistributor(RewardsDistributorInstance).setPeriodicRewards(root); + totalUSDCToDeposit += usdcToDeposit; + totalDAIToDeposit += daiToDeposit; + + deal(USDC, RewardsDistributorInstance, totalUSDCToDeposit); + deal(DAI, RewardsDistributorInstance, totalDAIToDeposit); + vm.stopPrank(); + } + + function _getVaultAddress() private pure returns (address) { + return address(uint160(SuperformId)); + } + + function _getP2pSignerSignature( + address _clientAddress, + uint48 _clientBasisPointsOfDeposit, + uint48 _clientBasisPointsOfProfit, + uint256 _sigDeadline + ) private view returns (bytes memory) { + // p2p signer signing + bytes32 hashForP2pSigner = factory.getHashForP2pSigner( + _clientAddress, _clientBasisPointsOfDeposit, _clientBasisPointsOfProfit, _sigDeadline + ); + bytes32 ethSignedMessageHashForP2pSigner = ECDSA.toEthSignedMessageHash(hashForP2pSigner); + (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(p2pSignerPrivateKey, ethSignedMessageHashForP2pSigner); + bytes memory p2pSignerSignature = abi.encodePacked(r2, s2, v2); + return p2pSignerSignature; + } + + function _doDeposit() private { + bytes memory p2pSignerSignature = + _getP2pSignerSignature(clientAddress, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline); + + vm.startPrank(clientAddress); + // Approve the proxy to spend USDT tokens + if (IERC20(USDT).allowance(clientAddress, proxyAddress) == 0) { + IERC20(USDT).safeApprove(proxyAddress, type(uint256).max); + } + + LiqRequest memory liqRequest = LiqRequest({ + txData: hex"4630a0d896ba9cffae8a22aa75ffdc6910e52d52ee9a199ee31eb8893dc693d7c89ed4a800000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000097116661c85c4e1ee35aa10f7fc5fe5e67b83a5b00000000000000000000000000000000000000000000000001a2c000701289810000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30783030303030303030303030303030303030303030303030303030303030303030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc0000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc00000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b2819000000000000000000000000000000000000000000000000000000000001e20800000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001842646478b00000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000000000000000000000000000000000000001e208000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b281900000000000000000000000000000000000000000000000001a2c000701289810000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000840294b008aa00579c1307b0ef2c499ad98a8ce58e5801ffff01962e23cd3f58f887a5238082a75d223f71890629006140b987d6b51fd75b66c3b07733beb5167c42fc010b2c639c533813f4aa9d7837caf62653d097ff8501ffff018ac2f9dac7a2852d44f3c09634444d533e4c078e011231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + token: USDT, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: DepositAmount, + outputAmount: SharesAmount, + maxSlippage: 500, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultDeposit, (req)); + + factory.deposit( + superformCalldata, ClientBasisPointsOfDeposit, ClientBasisPointsOfProfit, SigDeadline, p2pSignerSignature + ); + vm.stopPrank(); + } + + function _doWithdraw() private { + LiqRequest memory liqRequest = LiqRequest({ + txData: hex"4630a0d8db58392a5ec14b23ef56401c814f1ce0bff3fd4f7f74c06e092670b4c587c7a600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000fd35454f266dc9f672985260029f1686c6b6036c000000000000000000000000000000000000000000000000000000000000181a0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000d7375706572666f726d2e78797a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a30783030303030303030303030303030303030303030303030303030303030303030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc0000000000000000000000006140b987d6b51fd75b66c3b07733beb5167c42fc000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b281900000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000000000000000000000000000002c14939f0666fb00000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001442646478b000000000000000000000000c40f949f8a4e094d1b49a23ea9241d289b7b2819000000000000000000000000000000000000000000000000002c14939f0666fb00000000000000000000000094b008aa00579c1307b0ef2c499ad98a8ce58e58000000000000000000000000000000000000000000000000000000000000181a0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004202c40f949f8a4e094d1b49a23ea9241d289b7b281901ffff01e8a05463f7a2796e1bf11a25d317f17ed7fce5e7001231deb6f5749ef6ce6943a275a1d3e7486f4eae00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + token: USDT, + interimToken: address(0), + bridgeId: 101, + liqDstChainId: 10, + nativeAmount: 0 + }); + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: SuperformId, + amount: 12407523236013819, + outputAmount: 12407523236013819, + maxSlippage: 5000, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: proxyAddress, + receiverAddressSP: proxyAddress, + extraFormData: "" + }); + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + bytes memory superformCalldata = abi.encodeCall(IBaseRouter.singleDirectSingleVaultWithdraw, (req)); + + vm.startPrank(clientAddress); + P2pSuperformProxy(payable(proxyAddress)).withdraw(superformCalldata); + vm.stopPrank(); + } + + /// @dev Rolls & warps the given number of blocks forward the blockchain. + function _forward(uint256 blocks) internal { + vm.roll(block.number + blocks); + vm.warp(block.timestamp + blocks * 13); + } +} diff --git a/test/P2pSuperformProxyRewards.t.sol b/test/P2pSuperformProxyRewards.t.sol new file mode 100644 index 0000000..664770e --- /dev/null +++ b/test/P2pSuperformProxyRewards.t.sol @@ -0,0 +1,432 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { + P2pSuperformProxy, + P2pSuperformProxy__WithdrawAmountExceedsAccrued, + P2pSuperformProxy__NoAccruedRewards +} from "../src/adapters/superform/p2pSuperformProxy/P2pSuperformProxy.sol"; +import {IP2pYieldProxyFactory} from "../src/p2pYieldProxyFactory/IP2pYieldProxyFactory.sol"; +import {IAllowedCalldataChecker} from "../src/common/IAllowedCalldataChecker.sol"; +import "../src/adapters/superform/DataTypes.sol"; +import {IBaseRouter} from "../src/adapters/superform/IBaseRouter.sol"; +import {IBaseForm} from "../src/adapters/superform/IBaseForm.sol"; +import {Withdrawn} from "../src/structs/P2pStructs.sol"; +import {P2pYieldProxy__NotP2pOperator} from "../src/p2pYieldProxy/P2pYieldProxy.sol"; + +contract MockERC20 { + string public constant name = "Mock Token"; + string public constant symbol = "MOCK"; + uint8 public constant decimals = 18; + + mapping(address => uint256) private s_balances; + mapping(address => mapping(address => uint256)) private s_allowances; + uint256 public totalSupply; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function balanceOf(address account) external view returns (uint256) { + return s_balances[account]; + } + + function transfer(address to, uint256 amount) external returns (bool) { + _transfer(msg.sender, to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + uint256 allowed = s_allowances[from][msg.sender]; + require(allowed >= amount, "allowance"); + if (allowed != type(uint256).max) { + s_allowances[from][msg.sender] = allowed - amount; + } + _transfer(from, to, amount); + return true; + } + + function approve(address spender, uint256 amount) external returns (bool) { + s_allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return s_allowances[owner][spender]; + } + + function mint(address to, uint256 amount) external { + s_balances[to] += amount; + totalSupply += amount; + emit Transfer(address(0), to, amount); + } + + function _transfer(address from, address to, uint256 amount) internal { + require(to != address(0), "zero"); + uint256 balance = s_balances[from]; + require(balance >= amount, "balance"); + s_balances[from] = balance - amount; + s_balances[to] += amount; + emit Transfer(from, to, amount); + } +} + +contract MockAllowedCalldataChecker is IAllowedCalldataChecker { + function checkCalldata(address, bytes4, bytes calldata) external pure override {} +} + +contract MockRewardsDistributor { + function batchClaim(address, uint256[] calldata, address[][] calldata, uint256[][] calldata, bytes32[][] calldata) + external + {} +} + +contract MockFactory is IP2pYieldProxyFactory { + address private s_operator; + address private s_pendingOperator; + + constructor(address operator_) { + s_operator = operator_; + } + + function setP2pOperator(address operator_) external { + s_operator = operator_; + } + + function transferP2pOperator(address _newP2pOperator) external override { + s_pendingOperator = _newP2pOperator; + } + + function acceptP2pOperator() external override { + s_operator = s_pendingOperator; + s_pendingOperator = address(0); + } + + function getPendingP2pOperator() external view override returns (address) { + return s_pendingOperator; + } + + function deposit(bytes calldata, uint48, uint48, uint256, bytes calldata) + external + payable + override + returns (address) + { + revert("MockFactory:deposit"); + } + + function predictP2pYieldProxyAddress(address, uint48, uint48) external view override returns (address) { + return address(0); + } + + function transferP2pSigner(address) external override {} + + function getReferenceP2pYieldProxy() external view override returns (address) { + return address(0); + } + + function getHashForP2pSigner(address, uint48, uint48, uint256) external view override returns (bytes32) { + return bytes32(0); + } + + function getP2pSigner() external view override returns (address) { + return address(0); + } + + function getP2pOperator() external view override returns (address) { + return s_operator; + } + + function getAllProxies() external view override returns (address[] memory) { + return new address[](0); + } + + function supportsInterface(bytes4) external pure override returns (bool) { + return false; + } +} + +contract MockSuperPositions { + mapping(address => mapping(uint256 => uint256)) internal s_balances; + mapping(address => mapping(address => mapping(uint256 => uint256))) internal s_allowances; + + function setBalance(address owner, uint256 id, uint256 amount) external { + s_balances[owner][id] = amount; + } + + function balanceOf(address owner, uint256 id) external view returns (uint256) { + return s_balances[owner][id]; + } + + function increaseAllowance(address spender, uint256 id, uint256 addedValue) external returns (bool) { + s_allowances[msg.sender][spender][id] += addedValue; + return true; + } + + function useAllowance(address owner, address spender, uint256 id, uint256 amount) external { + uint256 allowed = s_allowances[owner][spender][id]; + require(allowed >= amount, "allowance"); + s_allowances[owner][spender][id] = allowed - amount; + + uint256 balance = s_balances[owner][id]; + require(balance >= amount, "balance"); + s_balances[owner][id] = balance - amount; + } +} + +contract MockBaseForm { + uint256 private s_sharePrice; + + constructor(uint256 sharePrice_) { + s_sharePrice = sharePrice_; + } + + function setSharePrice(uint256 sharePrice_) external { + s_sharePrice = sharePrice_; + } + + function previewRedeemFrom(uint256 shares_) external view returns (uint256) { + return shares_ * s_sharePrice / 1e18; + } + + function previewWithdrawFrom(uint256 assets_) external view returns (uint256) { + return assets_ * 1e18 / s_sharePrice; + } +} + +contract MockYieldProtocol { + MockSuperPositions internal immutable i_superPositions; + MockBaseForm internal immutable i_baseForm; + MockERC20 internal immutable i_asset; + + constructor(MockSuperPositions superPositions_, MockBaseForm baseForm_, MockERC20 asset_) { + i_superPositions = superPositions_; + i_baseForm = baseForm_; + i_asset = asset_; + } + + function singleDirectSingleVaultWithdraw(SingleDirectSingleVaultStateReq memory req_) external { + i_superPositions.useAllowance( + msg.sender, address(this), req_.superformData.superformId, req_.superformData.amount + ); + uint256 assetsToSend = i_baseForm.previewRedeemFrom(req_.superformData.amount); + require(i_asset.transfer(req_.superformData.receiverAddress, assetsToSend), "transfer failed"); + } +} + +contract TestableP2pSuperformProxy is P2pSuperformProxy { + constructor( + address factory_, + address treasury_, + address router_, + address superPositions_, + address allowedCalldataChecker_, + address rewardsDistributor_ + ) P2pSuperformProxy(factory_, treasury_, router_, superPositions_, allowedCalldataChecker_, rewardsDistributor_) {} + + function testSetup(address client_, uint48 depositBps_, uint48 profitBps_) external { + s_client = payable(client_); + s_clientBasisPointsOfDeposit = depositBps_; + s_clientBasisPointsOfProfit = profitBps_; + } + + function setTotals(uint256 vaultId, address asset, uint256 deposited, uint256 withdrawn) external { + s_totalDeposited[vaultId][asset] = deposited; + s_totalWithdrawn[vaultId][asset] = + Withdrawn({amount: uint208(withdrawn), lastFeeCollectionTime: uint48(block.timestamp)}); + } +} + +contract P2pSuperformProxyRewardsTest is Test { + TestableP2pSuperformProxy internal proxy; + MockFactory internal factory; + MockSuperPositions internal superPositions; + MockBaseForm internal baseForm; + MockYieldProtocol internal yieldProtocol; + MockERC20 internal assetToken; + MockAllowedCalldataChecker internal calldataChecker; + MockRewardsDistributor internal rewardsDistributor; + + address internal constant OPERATOR = address(0xA11CE); + address internal constant CLIENT = address(0xB0B); + address internal constant TREASURY = address(0xC0FFEe); + + uint256 internal constant SHARE_PRICE = 1_250_000_000_000_000_000; // 1.25 * 1e18 + uint256 internal constant INITIAL_SHARES = 100 ether; + uint256 internal constant DEPOSIT_AMOUNT = 100 ether; + uint256 internal constant ACCRUED_REWARDS = 25 ether; + uint256 internal constant SHARES_FOR_REWARDS = 20 ether; + + uint256 internal vaultId; + + function setUp() public { + assetToken = new MockERC20(); + superPositions = new MockSuperPositions(); + baseForm = new MockBaseForm(SHARE_PRICE); + calldataChecker = new MockAllowedCalldataChecker(); + rewardsDistributor = new MockRewardsDistributor(); + factory = new MockFactory(OPERATOR); + yieldProtocol = new MockYieldProtocol(superPositions, baseForm, assetToken); + + proxy = new TestableP2pSuperformProxy( + address(factory), + TREASURY, + address(yieldProtocol), + address(superPositions), + address(calldataChecker), + address(rewardsDistributor) + ); + + vaultId = uint256(uint160(address(baseForm))); + + proxy.testSetup(CLIENT, 10_000, 8_000); + proxy.setTotals(vaultId, address(assetToken), DEPOSIT_AMOUNT, 0); + + superPositions.setBalance(address(proxy), vaultId, INITIAL_SHARES); + + assetToken.mint(address(yieldProtocol), 1_000 ether); + } + + function test_calculateAccruedRewardsMatchesPreview() public { + int256 rewards = proxy.calculateAccruedRewards(vaultId, address(assetToken)); + assertEq(rewards, int256(ACCRUED_REWARDS)); + } + + function test_withdrawAccruedRewards_DistributesAndResetsAccrual() public { + bytes memory withdrawCalldata = _buildWithdrawCalldata(SHARES_FOR_REWARDS); + + vm.prank(OPERATOR); + proxy.withdrawAccruedRewards(withdrawCalldata); + + uint256 expectedP2p = (ACCRUED_REWARDS * (10_000 - 8_000) + 9_999) / 10_000; + uint256 expectedClient = ACCRUED_REWARDS - expectedP2p; + + assertEq(assetToken.balanceOf(TREASURY), expectedP2p, "p2p treasury amount"); + assertEq(assetToken.balanceOf(CLIENT), expectedClient, "client amount"); + + int256 rewardsAfter = proxy.calculateAccruedRewards(vaultId, address(assetToken)); + assertEq(rewardsAfter, int256(0), "accrued rewards should reset"); + + assertEq( + superPositions.balanceOf(address(proxy), vaultId), INITIAL_SHARES - SHARES_FOR_REWARDS, "share balance" + ); + } + + function test_withdrawAccruedRewards_RevertIfExceedsAccrued() public { + uint256 excessiveShares = SHARES_FOR_REWARDS + 1 ether; + bytes memory withdrawCalldata = _buildWithdrawCalldata(excessiveShares); + + uint256 requestedAssets = IBaseForm(address(uint160(vaultId))).previewRedeemFrom(excessiveShares); + + vm.expectRevert( + abi.encodeWithSelector( + P2pSuperformProxy__WithdrawAmountExceedsAccrued.selector, requestedAssets, ACCRUED_REWARDS + ) + ); + vm.prank(OPERATOR); + proxy.withdrawAccruedRewards(withdrawCalldata); + } + + function test_withdrawAccruedRewards_RevertWhenNoAccruedRewards() public { + proxy.setTotals(vaultId, address(assetToken), 125 ether, 0); + + bytes memory withdrawCalldata = _buildWithdrawCalldata(SHARES_FOR_REWARDS); + + vm.expectRevert( + abi.encodeWithSelector(P2pSuperformProxy__NoAccruedRewards.selector, vaultId, address(assetToken)) + ); + vm.prank(OPERATOR); + proxy.withdrawAccruedRewards(withdrawCalldata); + } + + function test_withdrawAccruedRewards_RevertForNonOperator() public { + bytes memory withdrawCalldata = _buildWithdrawCalldata(SHARES_FOR_REWARDS); + + address notOperator = address(0xDEAD); + vm.expectRevert(abi.encodeWithSelector(P2pYieldProxy__NotP2pOperator.selector, notOperator)); + vm.prank(notOperator); + proxy.withdrawAccruedRewards(withdrawCalldata); + } + + function test_doubleFeeCollectionBug_SuperformFlow() public { + bytes memory rewardsWithdrawCalldata = _buildWithdrawCalldata(SHARES_FOR_REWARDS); + + vm.prank(OPERATOR); + proxy.withdrawAccruedRewards(rewardsWithdrawCalldata); + + uint256 treasuryAfterRewards = assetToken.balanceOf(TREASURY); + uint256 clientAfterRewards = assetToken.balanceOf(CLIENT); + + uint256 remainingShares = INITIAL_SHARES - SHARES_FOR_REWARDS; + bytes memory clientWithdrawCalldata = _buildWithdrawCalldata(remainingShares); + + vm.prank(CLIENT); + proxy.withdraw(clientWithdrawCalldata); + + uint256 clientPrincipalReceived = assetToken.balanceOf(CLIENT) - clientAfterRewards; + uint256 treasuryGainOnPrincipal = assetToken.balanceOf(TREASURY) - treasuryAfterRewards; + + assertEq(clientPrincipalReceived, DEPOSIT_AMOUNT, "client principal received"); + assertEq(treasuryGainOnPrincipal, 0, "treasury should not earn twice"); + } + + function test_PartialWithdraw() public { + uint256 partialShares = 10 ether; + bytes memory withdrawCalldata = _buildWithdrawCalldata(partialShares); + + uint256 treasuryBefore = assetToken.balanceOf(TREASURY); + uint256 clientBefore = assetToken.balanceOf(CLIENT); + + vm.prank(OPERATOR); + proxy.withdrawAccruedRewards(withdrawCalldata); + + uint256 actualWithdrawn = baseForm.previewRedeemFrom(partialShares); + uint256 treasuryReceived = assetToken.balanceOf(TREASURY) - treasuryBefore; + uint256 clientReceived = assetToken.balanceOf(CLIENT) - clientBefore; + + uint256 profitBps = proxy.getClientBasisPointsOfProfit(); + uint256 expectedFee = (actualWithdrawn * (10_000 - profitBps) + 9_999) / 10_000; + uint256 expectedClient = actualWithdrawn - expectedFee; + + uint256 treasuryDelta = + treasuryReceived > expectedFee ? treasuryReceived - expectedFee : expectedFee - treasuryReceived; + uint256 clientDelta = + clientReceived > expectedClient ? clientReceived - expectedClient : expectedClient - clientReceived; + + assertLe(treasuryDelta, 1, "Treasury fee overcharged"); + assertLe(clientDelta, 1, "Client lost funds"); + } + + function _buildWithdrawCalldata(uint256 sharesToWithdraw) internal view returns (bytes memory) { + LiqRequest memory liqRequest = LiqRequest({ + txData: "", + token: address(assetToken), + interimToken: address(0), + bridgeId: 0, + liqDstChainId: 0, + nativeAmount: 0 + }); + + SingleVaultSFData memory superformData = SingleVaultSFData({ + superformId: vaultId, + amount: sharesToWithdraw, + outputAmount: sharesToWithdraw, + maxSlippage: 0, + liqRequest: liqRequest, + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: address(proxy), + receiverAddressSP: address(proxy), + extraFormData: "" + }); + + SingleDirectSingleVaultStateReq memory req = SingleDirectSingleVaultStateReq({superformData: superformData}); + + return abi.encodeCall(IBaseRouter.singleDirectSingleVaultWithdraw, (req)); + } +} diff --git a/test/mocks/MockAllowedCalldataChecker.sol b/test/mocks/MockAllowedCalldataChecker.sol new file mode 100644 index 0000000..c06cba1 --- /dev/null +++ b/test/mocks/MockAllowedCalldataChecker.sol @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025 P2P Validator +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.27; + +import "../../src/@openzeppelin/contracts-upgradable/proxy/utils/Initializable.sol"; +import "../../src/common/IAllowedCalldataChecker.sol"; + +/// @title MockAllowedCalldataChecker +/// @author P2P Validator +/// @notice Mock. Do NOT deploy!! +contract MockAllowedCalldataChecker is IAllowedCalldataChecker, Initializable { + function initialize() public initializer { + // do nothing in this implementation + } + + /// @inheritdoc IAllowedCalldataChecker + function checkCalldata(address, bytes4, bytes calldata) public pure { + // don't revert + } +} diff --git a/test/utils/Error.sol b/test/utils/Error.sol new file mode 100644 index 0000000..4a28483 --- /dev/null +++ b/test/utils/Error.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.27; + +library Error { + ////////////////////////////////////////////////////////////// + // CONFIGURATION ERRORS // + ////////////////////////////////////////////////////////////// + ///@notice errors thrown in protocol setup + + /// @dev thrown if chain id exceeds max(uint64) + error BLOCK_CHAIN_ID_OUT_OF_BOUNDS(); // 7ecdf933 + + /// @dev thrown if not possible to revoke a role in broadcasting + error CANNOT_REVOKE_NON_BROADCASTABLE_ROLES(); // c1901a05 + + /// @dev thrown if not possible to revoke last admin + error CANNOT_REVOKE_LAST_ADMIN(); // a567a5a2 + + /// @dev thrown if trying to set again pseudo immutables in super registry + error DISABLED(); // acb78998 + + /// @dev thrown if rescue delay is not yet set for a chain + error DELAY_NOT_SET(); // d0b3066f + + /// @dev thrown if get native token price estimate in paymentHelper is 0 + error INVALID_NATIVE_TOKEN_PRICE(); // 991f334e + + /// @dev thrown if wormhole refund chain id is not set + error REFUND_CHAIN_ID_NOT_SET(); // 789bb36b + + /// @dev thrown if wormhole relayer is not set + error RELAYER_NOT_SET(); // e8baf999 + + /// @dev thrown if a role to be revoked is not assigned + error ROLE_NOT_ASSIGNED(); // 35c2f322 + + ////////////////////////////////////////////////////////////// + // AUTHORIZATION ERRORS // + ////////////////////////////////////////////////////////////// + ///@notice errors thrown if functions cannot be called + + /// COMMON AUTHORIZATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if caller is not address(this), internal call + error INVALID_INTERNAL_CALL(); // 6566f0ae + + /// @dev thrown if msg.sender is not a valid amb implementation + error NOT_AMB_IMPLEMENTATION(); // 00e1b7be + + /// @dev thrown if msg.sender is not an allowed broadcaster + error NOT_ALLOWED_BROADCASTER(); // 5ac755f1 + + /// @dev thrown if msg.sender is not broadcast amb implementation + error NOT_BROADCAST_AMB_IMPLEMENTATION(); // b3af61c9 + + /// @dev thrown if msg.sender is not broadcast state registry + error NOT_BROADCAST_REGISTRY(); // b0acaf9d + + /// @dev thrown if msg.sender is not core state registry + error NOT_CORE_STATE_REGISTRY(); // 8c1715e2 + + /// @dev thrown if msg.sender is not emergency admin + error NOT_EMERGENCY_ADMIN(); // 445b0c3a + + /// @dev thrown if msg.sender is not emergency queue + error NOT_EMERGENCY_QUEUE(); // 94949e62 + + /// @dev thrown if msg.sender is not minter + error NOT_MINTER(); // 00914334 + + /// @dev thrown if msg.sender is not minter state registry + error NOT_MINTER_STATE_REGISTRY_ROLE(); // ec1845b2 + + /// @dev thrown if msg.sender is not paymaster + error NOT_PAYMASTER(); // 74891f76 + + /// @dev thrown if msg.sender is not payment admin + error NOT_PAYMENT_ADMIN(); // 62108606 + + /// @dev thrown if msg.sender is not protocol admin + error NOT_PROTOCOL_ADMIN(); // dc855554 + + /// @dev thrown if msg.sender is not state registry + error NOT_STATE_REGISTRY(); // 60e6a64e + + /// @dev thrown if msg.sender is not super registry + error NOT_SUPER_REGISTRY(); // e7b6503f + + /// @dev thrown if msg.sender is not superform router + error NOT_SUPERFORM_ROUTER(); // 158a2f6b + + /// @dev thrown if msg.sender is not a superform + error NOT_SUPERFORM(); // 5279abe6 + + /// @dev thrown if msg.sender is not superform factory + error NOT_SUPERFORM_FACTORY(); // c6ca730c + + /// @dev thrown if msg.sender is not timelock form + error NOT_TIMELOCK_SUPERFORM(); // 41645ec2 + + /// @dev thrown if msg.sender is not timelock state registry + error NOT_TIMELOCK_STATE_REGISTRY(); // ebe1e902 + + /// @dev thrown if msg.sender is not user or disputer + error NOT_VALID_DISPUTER(); // d8f8c1c0 + + /// @dev thrown if the msg.sender is not privileged caller + error NOT_PRIVILEGED_CALLER(bytes32 role); // c2703cd6 + + /// STATE REGISTRY AUTHORIZATION ERRORS + /// --------------------------------------------------------- + + /// @dev layerzero adapter specific error, thrown if caller not layerzero endpoint + error CALLER_NOT_ENDPOINT(); // 785db8f2 + + /// @dev hyperlane adapter specific error, thrown if caller not hyperlane mailbox + error CALLER_NOT_MAILBOX(); // b3c9ad31 + + /// @dev wormhole relayer specific error, thrown if caller not wormhole relayer + error CALLER_NOT_RELAYER(); // 5aa1e945 + + /// @dev thrown if src chain sender is not valid + error INVALID_SRC_SENDER(); // 1d527bfc + + ////////////////////////////////////////////////////////////// + // INPUT VALIDATION ERRORS // + ////////////////////////////////////////////////////////////// + ///@notice errors thrown if input variables are not valid + + /// COMMON INPUT VALIDATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if there is an array length mismatch + error ARRAY_LENGTH_MISMATCH(); // 88adebd2 + + /// @dev thrown if payload id does not exist + error INVALID_PAYLOAD_ID(); // abb45946 + + /// @dev error thrown when msg value should be zero in certain payable functions + error MSG_VALUE_NOT_ZERO(); // 308f275d + + /// @dev thrown if amb ids length is 0 + error ZERO_AMB_ID_LENGTH(); // 308f275d + + /// @dev thrown if address input is address 0 + error ZERO_ADDRESS(); // 538ba4f9 + + /// @dev thrown if amount input is 0 + error ZERO_AMOUNT(); // 538ba4f9 + + /// @dev thrown if final token is address 0 + error ZERO_FINAL_TOKEN(); // 636b4ef2 + + /// @dev thrown if value input is 0 + error ZERO_INPUT_VALUE(); // 021b4ea1 + + /// SUPERFORM ROUTER INPUT VALIDATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if the vaults data is invalid + error INVALID_SUPERFORMS_DATA(); // 29c0f4af + + /// @dev thrown if receiver address is not set + error RECEIVER_ADDRESS_NOT_SET(); // abd840db + + /// SUPERFORM FACTORY INPUT VALIDATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if a form is not ERC165 compatible + error ERC165_UNSUPPORTED(); + + /// @dev thrown if a form is not form interface compatible + error FORM_INTERFACE_UNSUPPORTED(); + + /// @dev error thrown if form implementation address already exists + error FORM_IMPLEMENTATION_ALREADY_EXISTS(); + + /// @dev error thrown if form implementation id already exists + error FORM_IMPLEMENTATION_ID_ALREADY_EXISTS(); + + /// @dev thrown if a form does not exist + error FORM_DOES_NOT_EXIST(); + + /// @dev thrown if form id is larger than max uint16 + error INVALID_FORM_ID(); + + /// @dev thrown if superform not on factory + error SUPERFORM_ID_NONEXISTENT(); + + /// @dev thrown if same vault and form implementation is used to create new superform + error VAULT_FORM_IMPLEMENTATION_COMBINATION_EXISTS(); + + /// FORM INPUT VALIDATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if in case of no txData, if liqData.token != vault.asset() + /// in case of txData, if token output of swap != vault.asset() + error DIFFERENT_TOKENS(); + + /// @dev thrown if the amount in direct withdraw is not correct + error DIRECT_WITHDRAW_INVALID_LIQ_REQUEST(); + + /// @dev thrown if the amount in xchain withdraw is not correct + error XCHAIN_WITHDRAW_INVALID_LIQ_REQUEST(); + + /// LIQUIDITY BRIDGE INPUT VALIDATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if route id is blacklisted in socket + error BLACKLISTED_ROUTE_ID(); + + /// @dev thrown if route id is not blacklisted in socket + error NOT_BLACKLISTED_ROUTE_ID(); + + /// @dev error thrown when txData selector of lifi bridge is a blacklisted selector + error BLACKLISTED_SELECTOR(); + + /// @dev error thrown when txData selector of lifi bridge is not a blacklisted selector + error NOT_BLACKLISTED_SELECTOR(); + + /// @dev thrown if a certain action of the user is not allowed given the txData provided + error INVALID_ACTION(); + + /// @dev thrown if in deposits, the liqDstChainId doesn't match the stateReq dstChainId + error INVALID_DEPOSIT_LIQ_DST_CHAIN_ID(); + + /// @dev thrown if index is invalid + error INVALID_INDEX(); + + /// @dev thrown if the chain id in the txdata is invalid + error INVALID_TXDATA_CHAIN_ID(); + + /// @dev thrown if the validation of bridge txData fails due to a destination call present + error INVALID_TXDATA_NO_DESTINATIONCALL_ALLOWED(); + + /// @dev thrown if the validation of bridge txData fails due to wrong receiver + error INVALID_TXDATA_RECEIVER(); + + /// @dev thrown if the validation of bridge txData fails due to wrong token + error INVALID_TXDATA_TOKEN(); + + /// @dev thrown if txData is not present (in case of xChain actions) + error NO_TXDATA_PRESENT(); + + /// STATE REGISTRY INPUT VALIDATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if payload is being updated with final amounts length different than amounts length + error DIFFERENT_PAYLOAD_UPDATE_AMOUNTS_LENGTH(); + + /// @dev thrown if payload is being updated with tx data length different than liq data length + error DIFFERENT_PAYLOAD_UPDATE_TX_DATA_LENGTH(); + + /// @dev thrown if keeper update final token is different than the vault underlying + error INVALID_UPDATE_FINAL_TOKEN(); + + /// @dev thrown if broadcast finality for wormhole is invalid + error INVALID_BROADCAST_FINALITY(); + + /// @dev thrown if amb id is not valid leading to an address 0 of the implementation + error INVALID_BRIDGE_ID(); + + /// @dev thrown if chain id involved in xchain message is invalid + error INVALID_CHAIN_ID(); + + /// @dev thrown if payload update amount isn't equal to dst swapper amount + error INVALID_DST_SWAP_AMOUNT(); + + /// @dev thrown if message amb and proof amb are the same + error INVALID_PROOF_BRIDGE_ID(); + + /// @dev thrown if order of proof AMBs is incorrect, either duplicated or not incrementing + error INVALID_PROOF_BRIDGE_IDS(); + + /// @dev thrown if rescue data lengths are invalid + error INVALID_RESCUE_DATA(); + + /// @dev thrown if delay is invalid + error INVALID_TIMELOCK_DELAY(); + + /// @dev thrown if amounts being sent in update payload mean a negative slippage + error NEGATIVE_SLIPPAGE(); + + /// @dev thrown if slippage is outside of bounds + error SLIPPAGE_OUT_OF_BOUNDS(); + + /// SUPERPOSITION INPUT VALIDATION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if src senders mismatch in state sync + error SRC_SENDER_MISMATCH(); + + /// @dev thrown if src tx types mismatch in state sync + error SRC_TX_TYPE_MISMATCH(); + + ////////////////////////////////////////////////////////////// + // EXECUTION ERRORS // + ////////////////////////////////////////////////////////////// + ///@notice errors thrown due to function execution logic + + /// COMMON EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if the swap in a direct deposit resulted in insufficient tokens + error DIRECT_DEPOSIT_SWAP_FAILED(); + + /// @dev thrown if payload is not unique + error DUPLICATE_PAYLOAD(); + + /// @dev thrown if native tokens fail to be sent to superform contracts + error FAILED_TO_SEND_NATIVE(); + + /// @dev thrown if allowance is not correct to deposit + error INSUFFICIENT_ALLOWANCE_FOR_DEPOSIT(); + + /// @dev thrown if contract has insufficient balance for operations + error INSUFFICIENT_BALANCE(); + + /// @dev thrown if native amount is not at least equal to the amount in the request + error INSUFFICIENT_NATIVE_AMOUNT(); + + /// @dev thrown if payload cannot be decoded + error INVALID_PAYLOAD(); + + /// @dev thrown if payload status is invalid + error INVALID_PAYLOAD_STATUS(); + + /// @dev thrown if payload type is invalid + error INVALID_PAYLOAD_TYPE(); + + /// LIQUIDITY BRIDGE EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if we try to decode the final swap output token in a xChain liquidity bridging action + error CANNOT_DECODE_FINAL_SWAP_OUTPUT_TOKEN(); + + /// @dev thrown if liquidity bridge fails for erc20 or native tokens + error FAILED_TO_EXECUTE_TXDATA(address token); + + /// @dev thrown if asset being used for deposit mismatches in multivault deposits + error INVALID_DEPOSIT_TOKEN(); + + /// STATE REGISTRY EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if bridge tokens haven't arrived to destination + error BRIDGE_TOKENS_PENDING(); + + /// @dev thrown if withdrawal tx data cannot be updated + error CANNOT_UPDATE_WITHDRAW_TX_DATA(); + + /// @dev thrown if rescue passed dispute deadline + error DISPUTE_TIME_ELAPSED(); + + /// @dev thrown if message failed to reach the specified level of quorum needed + error INSUFFICIENT_QUORUM(); + + /// @dev thrown if broadcast payload is invalid + error INVALID_BROADCAST_PAYLOAD(); + + /// @dev thrown if broadcast fee is invalid + error INVALID_BROADCAST_FEE(); + + /// @dev thrown if retry fees is less than required + error INVALID_RETRY_FEE(); + + /// @dev thrown if broadcast message type is wrong + error INVALID_MESSAGE_TYPE(); + + /// @dev thrown if payload hash is invalid during `retryMessage` on Layezero implementation + error INVALID_PAYLOAD_HASH(); + + /// @dev thrown if update payload function was called on a wrong payload + error INVALID_PAYLOAD_UPDATE_REQUEST(); + + /// @dev thrown if a state registry id is 0 + error INVALID_REGISTRY_ID(); + + /// @dev thrown if a form state registry id is 0 + error INVALID_FORM_REGISTRY_ID(); + + /// @dev thrown if trying to finalize the payload but the withdraw is still locked + error LOCKED(); + + /// @dev thrown if payload is already updated (during xChain deposits) + error PAYLOAD_ALREADY_UPDATED(); + + /// @dev thrown if payload is already processed + error PAYLOAD_ALREADY_PROCESSED(); + + /// @dev thrown if payload is not in UPDATED state + error PAYLOAD_NOT_UPDATED(); + + /// @dev thrown if rescue is still in timelocked state + error RESCUE_LOCKED(); + + /// @dev thrown if rescue is already proposed + error RESCUE_ALREADY_PROPOSED(); + + /// @dev thrown if payload hash is zero during `retryMessage` on Layezero implementation + error ZERO_PAYLOAD_HASH(); + + /// DST SWAPPER EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if process dst swap is tried for processed payload id + error DST_SWAP_ALREADY_PROCESSED(); + + /// @dev thrown if indices have duplicates + error DUPLICATE_INDEX(); + + /// @dev thrown if failed dst swap is already updated + error FAILED_DST_SWAP_ALREADY_UPDATED(); + + /// @dev thrown if indices are out of bounds + error INDEX_OUT_OF_BOUNDS(); + + /// @dev thrown if failed swap token amount is 0 + error INVALID_DST_SWAPPER_FAILED_SWAP(); + + /// @dev thrown if failed swap token amount is not 0 and if token balance is less than amount (non zero) + error INVALID_DST_SWAPPER_FAILED_SWAP_NO_TOKEN_BALANCE(); + + /// @dev thrown if failed swap token amount is not 0 and if native amount is less than amount (non zero) + error INVALID_DST_SWAPPER_FAILED_SWAP_NO_NATIVE_BALANCE(); + + /// @dev forbid xChain deposits with destination swaps without interim token set (for user protection) + error INVALID_INTERIM_TOKEN(); + + /// @dev thrown if dst swap output is less than minimum expected + error INVALID_SWAP_OUTPUT(); + + /// FORM EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if try to forward 4626 share from the superform + error CANNOT_FORWARD_4646_TOKEN(); + + /// @dev thrown in KYCDAO form if no KYC token is present + error NO_VALID_KYC_TOKEN(); + + /// @dev thrown in forms where a certain functionality is not allowed or implemented + error NOT_IMPLEMENTED(); + + /// @dev thrown if form implementation is PAUSED, users cannot perform any action + error PAUSED(); + + /// @dev thrown if shares != deposit output or assets != redeem output when minting SuperPositions + error VAULT_IMPLEMENTATION_FAILED(); + + /// @dev thrown if withdrawal tx data is not updated + error WITHDRAW_TOKEN_NOT_UPDATED(); + + /// @dev thrown if withdrawal tx data is not updated + error WITHDRAW_TX_DATA_NOT_UPDATED(); + + /// @dev thrown when redeeming from vault yields zero collateral + error WITHDRAW_ZERO_COLLATERAL(); + + /// PAYMENT HELPER EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if chainlink is reporting an improper price + error CHAINLINK_MALFUNCTION(); + + /// @dev thrown if chainlink is reporting an incomplete round + error CHAINLINK_INCOMPLETE_ROUND(); + + /// @dev thrown if feed decimals is not 8 + error CHAINLINK_UNSUPPORTED_DECIMAL(); + + /// EMERGENCY QUEUE EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if emergency withdraw is not queued + error EMERGENCY_WITHDRAW_NOT_QUEUED(); + + /// @dev thrown if emergency withdraw is already processed + error EMERGENCY_WITHDRAW_PROCESSED_ALREADY(); + + /// SUPERPOSITION EXECUTION ERRORS + /// --------------------------------------------------------- + + /// @dev thrown if uri cannot be updated + error DYNAMIC_URI_FROZEN(); + + /// @dev thrown if tx history is not found while state sync + error TX_HISTORY_NOT_FOUND(); +} diff --git a/test/utils/merkle/helper/MerkleReader.sol b/test/utils/merkle/helper/MerkleReader.sol new file mode 100644 index 0000000..3a45c21 --- /dev/null +++ b/test/utils/merkle/helper/MerkleReader.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.23; + +import "../../../../lib/forge-std/src/Test.sol"; +import "../../../../src/@openzeppelin/contracts/utils/Strings.sol"; +import "forge-std/StdJson.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; +import {StdInvariant} from "forge-std/StdInvariant.sol"; + +abstract contract MerkleReader is StdCheats, StdInvariant, Test { + using stdJson for string; + + string private basePathForRoot = "/test/utils/merkle/target/jsGeneratedRoot"; + string private basePathForTreeDump = "/test/utils/merkle/target/jsTreeDump"; + + string private prepend = ".values["; + + string private claimerQueryAppend = "].claimer"; + + string private periodIdQueryAppend = "].periodId"; + + string private rewardTokensQueryAppend = "].rewardTokens"; + + string private amountsClaimedQueryAppend = "].amountsClaimed"; + + string private chainIdQueryAppend = "].chainId"; + + string private proofQueryAppend = "].proof"; + + address USDC = 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85; + address DAI = 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; + + address[] tokenTypes = [USDC, DAI]; + + struct LocalVars { + string rootJson; + bytes encodedRoot; + string treeJson; + bytes encodedClaimer; + bytes encodedPeriod; + bytes encodedRewardTokens; + bytes encodedAmountsClaimed; + bytes encodedChainId; + bytes encodedProof; + address claimer; + uint256 periodId; + address[] rewardTokens; + uint256[] amountsClaimed; + uint256 chainId; + } + + struct MerkleArgs { + uint256 periodId_; + address claimer_; + uint256 chainId_; + } + + /// @dev read the merkle root and proof from js generated tree + function _generateMerkleTree(MerkleArgs memory a) + internal + view + returns ( + bytes32 root, + uint256 claimers, + uint256 totalUSDCValue, + uint256 totalDAIValue, + bytes32[] memory proofsForIndex, + address[] memory tokensForIndex, + uint256[] memory amountsForIndex + ) + { + LocalVars memory v; + + v.rootJson = + vm.readFile(string.concat(vm.projectRoot(), basePathForRoot, Strings.toString(a.periodId_), ".json")); + v.encodedRoot = vm.parseJson(v.rootJson, ".root"); + root = abi.decode(v.encodedRoot, (bytes32)); + + v.treeJson = + vm.readFile(string.concat(vm.projectRoot(), basePathForTreeDump, Strings.toString(a.periodId_), ".json")); + + /// get the total elements to find out the right proof + bytes memory encodedValuesJson = vm.parseJson(v.treeJson, ".values[*]"); + string[] memory valuesArr = abi.decode(encodedValuesJson, (string[])); + claimers = valuesArr.length; + for (uint256 i; i < claimers; ++i) { + v.encodedClaimer = vm.parseJson(v.treeJson, string.concat(prepend, Strings.toString(i), claimerQueryAppend)); + v.encodedPeriod = vm.parseJson(v.treeJson, string.concat(prepend, Strings.toString(i), periodIdQueryAppend)); + v.encodedRewardTokens = + vm.parseJson(v.treeJson, string.concat(prepend, Strings.toString(i), rewardTokensQueryAppend)); + v.encodedAmountsClaimed = + vm.parseJson(v.treeJson, string.concat(prepend, Strings.toString(i), amountsClaimedQueryAppend)); + v.encodedChainId = vm.parseJson(v.treeJson, string.concat(prepend, Strings.toString(i), chainIdQueryAppend)); + + v.claimer = abi.decode(v.encodedClaimer, (address)); + v.periodId = abi.decode(v.encodedPeriod, (uint256)); + v.rewardTokens = abi.decode(v.encodedRewardTokens, (address[])); + v.amountsClaimed = abi.decode(v.encodedAmountsClaimed, (uint256[])); + v.chainId = abi.decode(v.encodedChainId, (uint256)); + + for (uint256 j; j < v.rewardTokens.length; ++j) { + if (v.rewardTokens[j] == tokenTypes[0]) { + totalUSDCValue += v.amountsClaimed[j]; + } else if (v.rewardTokens[j] == tokenTypes[1]) { + totalDAIValue += v.amountsClaimed[j]; + } + } + + if ( + a.claimer_ != address(0) && v.claimer == a.claimer_ && v.periodId == a.periodId_ + && v.chainId == a.chainId_ + ) { + v.encodedProof = vm.parseJson(v.treeJson, string.concat(prepend, Strings.toString(i), proofQueryAppend)); + proofsForIndex = abi.decode(v.encodedProof, (bytes32[])); + + tokensForIndex = v.rewardTokens; + amountsForIndex = v.amountsClaimed; + break; + } + } + } +} diff --git a/test/utils/merkle/target/input0.json b/test/utils/merkle/target/input0.json new file mode 100644 index 0000000..b3f7a07 --- /dev/null +++ b/test/utils/merkle/target/input0.json @@ -0,0 +1,198 @@ +{ + "types": [ + "address", + "uint256", + "address[]", + "uint256[]", + "uint256" + ], + "count": 16, + "values": { + "0": { + "0": "0x0000000000000000000000000000000000000001", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 644 + ], + "4": "10" + }, + "1": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "23", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 946 + ], + "4": "10" + }, + "2": { + "0": "0x0000000000000000000000000000000000000003", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 760, + 215 + ], + "4": "10" + }, + "3": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 124 + ], + "4": "10" + }, + "4": { + "0": "0x0000000000000000000000000000000000000005", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 529 + ], + "4": "10" + }, + "5": { + "0": "0x0000000000000000000000000000000000000006", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 465 + ], + "4": "10" + }, + "6": { + "0": "0x0000000000000000000000000000000000000007", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 338, + 478 + ], + "4": "10" + }, + "7": { + "0": "0x0000000000000000000000000000000000000008", + "1": "23", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 124 + ], + "4": "10" + }, + "8": { + "0": "0x0000000000000000000000000000000000000009", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 592, + 569 + ], + "4": "10" + }, + "9": { + "0": "0x0000000000000000000000000000000000000010", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 760 + ], + "4": "10" + }, + "10": { + "0": "0x0000000000000000000000000000000000000011", + "1": "23", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 222 + ], + "4": "10" + }, + "11": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "23", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 718 + ], + "4": "10" + }, + "12": { + "0": "0x0000000000000000000000000000000000000013", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 427, + 485 + ], + "4": "10" + }, + "13": { + "0": "0x0000000000000000000000000000000000000014", + "1": "23", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 710, + 629 + ], + "4": "10" + }, + "14": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "23", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 244 + ], + "4": "10" + }, + "15": { + "0": "0x0000000000000000000000000000000000000016", + "1": "23", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 440 + ], + "4": "10" + } + } +} \ No newline at end of file diff --git a/test/utils/merkle/target/input1.json b/test/utils/merkle/target/input1.json new file mode 100644 index 0000000..06a26ce --- /dev/null +++ b/test/utils/merkle/target/input1.json @@ -0,0 +1,198 @@ +{ + "types": [ + "address", + "uint256", + "address[]", + "uint256[]", + "uint256" + ], + "count": 16, + "values": { + "0": { + "0": "0x0000000000000000000000000000000000000001", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 815, + 866 + ], + "4": "10" + }, + "1": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "24", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 13 + ], + "4": "10" + }, + "2": { + "0": "0x0000000000000000000000000000000000000003", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 309, + 877 + ], + "4": "10" + }, + "3": { + "0": "0x0000000000000000000000000000000000000004", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 577 + ], + "4": "10" + }, + "4": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 388, + 304 + ], + "4": "10" + }, + "5": { + "0": "0x0000000000000000000000000000000000000006", + "1": "24", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 498 + ], + "4": "10" + }, + "6": { + "0": "0x0000000000000000000000000000000000000007", + "1": "24", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 790 + ], + "4": "10" + }, + "7": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 166 + ], + "4": "10" + }, + "8": { + "0": "0x0000000000000000000000000000000000000009", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 496 + ], + "4": "10" + }, + "9": { + "0": "0x0000000000000000000000000000000000000010", + "1": "24", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 14 + ], + "4": "10" + }, + "10": { + "0": "0x0000000000000000000000000000000000000011", + "1": "24", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 307 + ], + "4": "10" + }, + "11": { + "0": "0x0000000000000000000000000000000000000012", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 478, + 561 + ], + "4": "10" + }, + "12": { + "0": "0x0000000000000000000000000000000000000013", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 924, + 372 + ], + "4": "10" + }, + "13": { + "0": "0x0000000000000000000000000000000000000014", + "1": "24", + "2": [ + "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" + ], + "3": [ + 76 + ], + "4": "10" + }, + "14": { + "0": "0xFd35454f266dC9f672985260029f1686c6b6036c", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 533 + ], + "4": "10" + }, + "15": { + "0": "0x0000000000000000000000000000000000000016", + "1": "24", + "2": [ + "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" + ], + "3": [ + 30 + ], + "4": "10" + } + } +} \ No newline at end of file diff --git a/test/utils/merkle/target/jsGeneratedRoot23.json b/test/utils/merkle/target/jsGeneratedRoot23.json new file mode 100644 index 0000000..2651c3a --- /dev/null +++ b/test/utils/merkle/target/jsGeneratedRoot23.json @@ -0,0 +1 @@ +{"root":"0x2bf43b9d8e1845e676173d3ca213b17c5c44740d6a74b03469cb880dd894cb9e"} \ No newline at end of file diff --git a/test/utils/merkle/target/jsGeneratedRoot24.json b/test/utils/merkle/target/jsGeneratedRoot24.json new file mode 100644 index 0000000..05d9599 --- /dev/null +++ b/test/utils/merkle/target/jsGeneratedRoot24.json @@ -0,0 +1 @@ +{"root":"0xa7ac430d24b4f578be02ce6f1b5548e2bee15f0cb66286db497a6e141caab452"} \ No newline at end of file diff --git a/test/utils/merkle/target/jsTreeDump23.json b/test/utils/merkle/target/jsTreeDump23.json new file mode 100644 index 0000000..d633b87 --- /dev/null +++ b/test/utils/merkle/target/jsTreeDump23.json @@ -0,0 +1 @@ +{"format":"standard-v1","leafEncoding":["address","uint256","address[]","uint256[]","uint256"],"tree":["0x2bf43b9d8e1845e676173d3ca213b17c5c44740d6a74b03469cb880dd894cb9e","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be","0xe11a552bab8f88adb419d87280e1269dda06b170fbcb3a43466a9a37d0400f5d","0x932634fb4cbd73fb7f50b6af5179691b287c32decf9195ccb0e97798f68f887c","0x06179d2959e056a83da68001a0c0e71e1121502a37b1144f3012f98cabbf0426","0x5e57c001c65d48f87f318e7341a8dc07aa1818db0a39cc5896ab17e7a8c64285","0x986f255b73f40cfaa97c8db5948a47cc58eafdd556c6168ec364d972117007c5","0x1ea33d0eecc5667ff03ab56ecbb74114db6543a0b9de53d44554752ad35336c1","0xde9aabae854e8eab5d808d7b53b17699b403c6b0693e6fe5339b6031d9f016bd","0xf0bbab671e3bedd8497f3f426aa062bc15ea74acdacc2a03dcb55ca793a9e330","0x4ff7e6e0ed2151dc50c6b86491c2159277b349fe2a36cb622d382e0218e682b9","0xda77cc8421b1a20162ef646a3d49eca91c2265c906e1d2e9bff4003f9aa9173a","0x5306a18bedbef6749dc20cb109ac6df8e91d1f06aa66c1e2756c879d4c145e78","0x9389b61a1150ffa494f9c432345da94070ac3ba27ab3a2bebc37fd5210c98e3e","0xfb376675373628634a4b67f4bc9fa4f7b43deab7023fa8ef5f4beabc1aeb7f3b","0xd798ad64ee24bca69e3342c120c55613057a7a21309c3d32d796ac4d10cb8144","0xc62bf2d03d26ad3e34ea98d52708396ae7ba865ec8ca8186295768d04fd9b1d8","0xc0be70bc2da24f98175252971d22881237734c17eb19d8056a46da7f22dd7d35","0xb12e4c92b39a81d1726f08d7ab653a81eab1f16d8d4cd5f7141c9fc75bd3282c","0x98e9c95c8a89c96f6f29bd4ffbed821409f3bba5ca96f2a685e40ab87aa181b0","0x91c461e1950670f16bc699746e1bfeb33206ef78a34866fcbc2629e92f11062f","0x75d6224377671b4872c94f5d2fa4db2819b751ae8b34fa0e8ba5d72ad4c83e30","0x5f83f063c2ea2d978f1ae0201c27512b53256a3376cad383c32106f072ba4e18","0x4e7ac259408271556aac94fd0194ad26da4ef30dc6a5432e579ca8c707b032db","0x4c0c7def04f707cf14a0699744e7c815362b47ad1f730570a0239f510d77014b","0x418874594a3c112ebe5f933a55f8cab832c4ed3680f9d64a048a50d78167dc61","0x1fb24815ba1d63bfb7efdc7cfda3f94a050ac7012d2b072ab982cb6e13cb9fd9","0x15f4ffa82ec2cdf9868706176daea0358f6694aa9cdc2e309ad53f66be6a5a9f","0x028fb603d35bb3b86844b96d797ca632d2cbc7d840176c7bc13a10bd60949eff","0x019b06e533beec5aad697b8ad2bb2f0f77d3200304d933cc75fadac1aeec119a"],"values":[{"value":["0x0000000000000000000000000000000000000001","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[644],"10"],"treeIndex":24,"claimer":"0x0000000000000000000000000000000000000001","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[644],"chainId":10,"proof":["0x5f83f063c2ea2d978f1ae0201c27512b53256a3376cad383c32106f072ba4e18","0xda77cc8421b1a20162ef646a3d49eca91c2265c906e1d2e9bff4003f9aa9173a","0x5e57c001c65d48f87f318e7341a8dc07aa1818db0a39cc5896ab17e7a8c64285","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","23",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[946],"10"],"treeIndex":19,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":23,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[946],"chainId":10,"proof":["0x98e9c95c8a89c96f6f29bd4ffbed821409f3bba5ca96f2a685e40ab87aa181b0","0xf0bbab671e3bedd8497f3f426aa062bc15ea74acdacc2a03dcb55ca793a9e330","0xe11a552bab8f88adb419d87280e1269dda06b170fbcb3a43466a9a37d0400f5d","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]},{"value":["0x0000000000000000000000000000000000000003","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[760,215],"10"],"treeIndex":17,"claimer":"0x0000000000000000000000000000000000000003","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[760,215],"chainId":10,"proof":["0xc0be70bc2da24f98175252971d22881237734c17eb19d8056a46da7f22dd7d35","0x986f255b73f40cfaa97c8db5948a47cc58eafdd556c6168ec364d972117007c5","0x932634fb4cbd73fb7f50b6af5179691b287c32decf9195ccb0e97798f68f887c","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[124],"10"],"treeIndex":16,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[124],"chainId":10,"proof":["0xfb376675373628634a4b67f4bc9fa4f7b43deab7023fa8ef5f4beabc1aeb7f3b","0x1ea33d0eecc5667ff03ab56ecbb74114db6543a0b9de53d44554752ad35336c1","0x932634fb4cbd73fb7f50b6af5179691b287c32decf9195ccb0e97798f68f887c","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]},{"value":["0x0000000000000000000000000000000000000005","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[529],"10"],"treeIndex":25,"claimer":"0x0000000000000000000000000000000000000005","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[529],"chainId":10,"proof":["0x418874594a3c112ebe5f933a55f8cab832c4ed3680f9d64a048a50d78167dc61","0x4ff7e6e0ed2151dc50c6b86491c2159277b349fe2a36cb622d382e0218e682b9","0x5e57c001c65d48f87f318e7341a8dc07aa1818db0a39cc5896ab17e7a8c64285","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0x0000000000000000000000000000000000000006","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[465],"10"],"treeIndex":30,"claimer":"0x0000000000000000000000000000000000000006","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[465],"chainId":10,"proof":["0x028fb603d35bb3b86844b96d797ca632d2cbc7d840176c7bc13a10bd60949eff","0x5306a18bedbef6749dc20cb109ac6df8e91d1f06aa66c1e2756c879d4c145e78","0x06179d2959e056a83da68001a0c0e71e1121502a37b1144f3012f98cabbf0426","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0x0000000000000000000000000000000000000007","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[338,478],"10"],"treeIndex":28,"claimer":"0x0000000000000000000000000000000000000007","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[338,478],"chainId":10,"proof":["0x1fb24815ba1d63bfb7efdc7cfda3f94a050ac7012d2b072ab982cb6e13cb9fd9","0x9389b61a1150ffa494f9c432345da94070ac3ba27ab3a2bebc37fd5210c98e3e","0x06179d2959e056a83da68001a0c0e71e1121502a37b1144f3012f98cabbf0426","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0x0000000000000000000000000000000000000008","23",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[124],"10"],"treeIndex":26,"claimer":"0x0000000000000000000000000000000000000008","periodId":23,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[124],"chainId":10,"proof":["0x4c0c7def04f707cf14a0699744e7c815362b47ad1f730570a0239f510d77014b","0x4ff7e6e0ed2151dc50c6b86491c2159277b349fe2a36cb622d382e0218e682b9","0x5e57c001c65d48f87f318e7341a8dc07aa1818db0a39cc5896ab17e7a8c64285","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0x0000000000000000000000000000000000000009","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[592,569],"10"],"treeIndex":20,"claimer":"0x0000000000000000000000000000000000000009","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[592,569],"chainId":10,"proof":["0xb12e4c92b39a81d1726f08d7ab653a81eab1f16d8d4cd5f7141c9fc75bd3282c","0xf0bbab671e3bedd8497f3f426aa062bc15ea74acdacc2a03dcb55ca793a9e330","0xe11a552bab8f88adb419d87280e1269dda06b170fbcb3a43466a9a37d0400f5d","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]},{"value":["0x0000000000000000000000000000000000000010","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[760],"10"],"treeIndex":18,"claimer":"0x0000000000000000000000000000000000000010","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[760],"chainId":10,"proof":["0xc62bf2d03d26ad3e34ea98d52708396ae7ba865ec8ca8186295768d04fd9b1d8","0x986f255b73f40cfaa97c8db5948a47cc58eafdd556c6168ec364d972117007c5","0x932634fb4cbd73fb7f50b6af5179691b287c32decf9195ccb0e97798f68f887c","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]},{"value":["0x0000000000000000000000000000000000000011","23",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[222],"10"],"treeIndex":22,"claimer":"0x0000000000000000000000000000000000000011","periodId":23,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[222],"chainId":10,"proof":["0x91c461e1950670f16bc699746e1bfeb33206ef78a34866fcbc2629e92f11062f","0xde9aabae854e8eab5d808d7b53b17699b403c6b0693e6fe5339b6031d9f016bd","0xe11a552bab8f88adb419d87280e1269dda06b170fbcb3a43466a9a37d0400f5d","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","23",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[718],"10"],"treeIndex":21,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":23,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[718],"chainId":10,"proof":["0x75d6224377671b4872c94f5d2fa4db2819b751ae8b34fa0e8ba5d72ad4c83e30","0xde9aabae854e8eab5d808d7b53b17699b403c6b0693e6fe5339b6031d9f016bd","0xe11a552bab8f88adb419d87280e1269dda06b170fbcb3a43466a9a37d0400f5d","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]},{"value":["0x0000000000000000000000000000000000000013","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[427,485],"10"],"treeIndex":23,"claimer":"0x0000000000000000000000000000000000000013","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[427,485],"chainId":10,"proof":["0x4e7ac259408271556aac94fd0194ad26da4ef30dc6a5432e579ca8c707b032db","0xda77cc8421b1a20162ef646a3d49eca91c2265c906e1d2e9bff4003f9aa9173a","0x5e57c001c65d48f87f318e7341a8dc07aa1818db0a39cc5896ab17e7a8c64285","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0x0000000000000000000000000000000000000014","23",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[710,629],"10"],"treeIndex":29,"claimer":"0x0000000000000000000000000000000000000014","periodId":23,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[710,629],"chainId":10,"proof":["0x019b06e533beec5aad697b8ad2bb2f0f77d3200304d933cc75fadac1aeec119a","0x5306a18bedbef6749dc20cb109ac6df8e91d1f06aa66c1e2756c879d4c145e78","0x06179d2959e056a83da68001a0c0e71e1121502a37b1144f3012f98cabbf0426","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","23",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[244],"10"],"treeIndex":27,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":23,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[244],"chainId":10,"proof":["0x15f4ffa82ec2cdf9868706176daea0358f6694aa9cdc2e309ad53f66be6a5a9f","0x9389b61a1150ffa494f9c432345da94070ac3ba27ab3a2bebc37fd5210c98e3e","0x06179d2959e056a83da68001a0c0e71e1121502a37b1144f3012f98cabbf0426","0x4ef52b39bea7aec2afeaf94b27a4c1e76269da449393c266a56d241e1c8f99b1"]},{"value":["0x0000000000000000000000000000000000000016","23",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[440],"10"],"treeIndex":15,"claimer":"0x0000000000000000000000000000000000000016","periodId":23,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[440],"chainId":10,"proof":["0xd798ad64ee24bca69e3342c120c55613057a7a21309c3d32d796ac4d10cb8144","0x1ea33d0eecc5667ff03ab56ecbb74114db6543a0b9de53d44554752ad35336c1","0x932634fb4cbd73fb7f50b6af5179691b287c32decf9195ccb0e97798f68f887c","0x3a63d1b314bfbf5b5609ad9b9300ab1f26c1428448f1ca1612d3b429d6c1e7be"]}]} \ No newline at end of file diff --git a/test/utils/merkle/target/jsTreeDump24.json b/test/utils/merkle/target/jsTreeDump24.json new file mode 100644 index 0000000..085ab70 --- /dev/null +++ b/test/utils/merkle/target/jsTreeDump24.json @@ -0,0 +1 @@ +{"format":"standard-v1","leafEncoding":["address","uint256","address[]","uint256[]","uint256"],"tree":["0xa7ac430d24b4f578be02ce6f1b5548e2bee15f0cb66286db497a6e141caab452","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000","0xbea654f18382b3f24e4fc53921431a0f24b1c9a5c0eae8cf46775ae8f6aa1163","0x337f98b7baddf65eafce39f5ac5d884ceff83d3d75e45bdb005679814d851682","0xa74576670005183bbe34e18c8fcc5c1afd77bd99134ca26bc48fe29710f064e5","0x760b76a12340696370287ac0fea4e39dfe3e451f5390252891174b4f4f0a8eca","0x65b9297bb9a4b73abc45b3a24168b91bcab2cca2abd91b1512c1f06b33ef07d1","0x65d39e0c48b9cccda676d5e49ed80af3bb2a68251c6b5de3a52205b2cb0d3334","0x0a6708693973ea469da5340a3b3bb4cc0544efa5e725fc976a269a8f32099e66","0xf4d05e0a4bda55ba5f27d9e94590a35d5e2ca1a2faac2a9b0b41a74e841f2526","0xe9900a68a1549d8fec8349260de081cb229f42940c243f9ee0bf743d9b9ee28a","0xe11feec3d14b071d7801e4b6fb046f229090931fb2bf0c2e13c4caafd3d46dd2","0xd714c81b0820b4076798b96e9a5abbab62fdc1b1dc7b966603e783a16f8c850c","0xaac9afe2f2bdbe480db4c75fc4bde587ce1da427d43fe92b351cf99a258e14b8","0xfcde80f36ba579ea80bbc383acb1e4cc423ba33c41d4bde2a068747b0dfdc37f","0xf6f35ef1294a1c4e09866b4e356ab39915cc6d222532cde68470c16cab4d04dc","0xd5b00c2f4b56531dfac15a0d3956d5adac04a628250868ba3e3d3071562c7b56","0xce87f62cbc4e1c7bd2c7e28f473467e194ff72a147bea90b651413b4f834bcb1","0xbeda7d6d3768b215e9a1a770215b974b0cee373e6eb1ed81768df698d2758590","0xbd102398aa029bc264af387cc8b83c3296f36263b6bc2c2b9ebf765ad8d314c0","0x9c6b712eb6f61d531522938a8663b90b6cd18e84b5da2c988aa46e3c829169f4","0x95d961ec71fcd0b91a0404a449d2cbd574e22ef1184642fdadfd60e69b46a3e0","0x9335e7af3a2494bacba1146bd1371a61c095badb3633925cfada3f4771024ad9","0x4adda7e1e9d1c72615d7ff8708376044a621b4def1a6da9198832b37d2c59fbf","0x2d7e748e5afbcf9157ebb0572295fbe19d059245281f1edd8789c61c7126608a","0x26f4c72fa460ae75b150377e980cc3daaf1055e63d3105bb43b6864397ddfb9e","0x26cdf9b8244732226c2b03cd1a9b4690ef2774b65529e1f9cca28c5cb09191e1","0x1629f910b1f9fd981142f5f9ba965d5aaa0a6dd4cd1bf6e64ceb06f3ee60fd44","0x092bae9e0707af1f27496823f205004de063d11cf539111e45cae5392a119798","0x052d46a76f1a529f26f38829dfae3f79e99f589af5b413d3c017ac2efabd6345"],"values":[{"value":["0x0000000000000000000000000000000000000001","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[815,866],"10"],"treeIndex":17,"claimer":"0x0000000000000000000000000000000000000001","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[815,866],"chainId":10,"proof":["0xce87f62cbc4e1c7bd2c7e28f473467e194ff72a147bea90b651413b4f834bcb1","0x65b9297bb9a4b73abc45b3a24168b91bcab2cca2abd91b1512c1f06b33ef07d1","0x337f98b7baddf65eafce39f5ac5d884ceff83d3d75e45bdb005679814d851682","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","24",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[13],"10"],"treeIndex":24,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":24,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[13],"chainId":10,"proof":["0x9335e7af3a2494bacba1146bd1371a61c095badb3633925cfada3f4771024ad9","0xe11feec3d14b071d7801e4b6fb046f229090931fb2bf0c2e13c4caafd3d46dd2","0x760b76a12340696370287ac0fea4e39dfe3e451f5390252891174b4f4f0a8eca","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0x0000000000000000000000000000000000000003","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[309,877],"10"],"treeIndex":16,"claimer":"0x0000000000000000000000000000000000000003","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[309,877],"chainId":10,"proof":["0xfcde80f36ba579ea80bbc383acb1e4cc423ba33c41d4bde2a068747b0dfdc37f","0x65d39e0c48b9cccda676d5e49ed80af3bb2a68251c6b5de3a52205b2cb0d3334","0x337f98b7baddf65eafce39f5ac5d884ceff83d3d75e45bdb005679814d851682","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]},{"value":["0x0000000000000000000000000000000000000004","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[577],"10"],"treeIndex":25,"claimer":"0x0000000000000000000000000000000000000004","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[577],"chainId":10,"proof":["0x26f4c72fa460ae75b150377e980cc3daaf1055e63d3105bb43b6864397ddfb9e","0xe9900a68a1549d8fec8349260de081cb229f42940c243f9ee0bf743d9b9ee28a","0x760b76a12340696370287ac0fea4e39dfe3e451f5390252891174b4f4f0a8eca","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[388,304],"10"],"treeIndex":21,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[388,304],"chainId":10,"proof":["0x95d961ec71fcd0b91a0404a449d2cbd574e22ef1184642fdadfd60e69b46a3e0","0x0a6708693973ea469da5340a3b3bb4cc0544efa5e725fc976a269a8f32099e66","0xbea654f18382b3f24e4fc53921431a0f24b1c9a5c0eae8cf46775ae8f6aa1163","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]},{"value":["0x0000000000000000000000000000000000000006","24",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[498],"10"],"treeIndex":23,"claimer":"0x0000000000000000000000000000000000000006","periodId":24,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[498],"chainId":10,"proof":["0x4adda7e1e9d1c72615d7ff8708376044a621b4def1a6da9198832b37d2c59fbf","0xe11feec3d14b071d7801e4b6fb046f229090931fb2bf0c2e13c4caafd3d46dd2","0x760b76a12340696370287ac0fea4e39dfe3e451f5390252891174b4f4f0a8eca","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0x0000000000000000000000000000000000000007","24",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[790],"10"],"treeIndex":28,"claimer":"0x0000000000000000000000000000000000000007","periodId":24,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[790],"chainId":10,"proof":["0x26cdf9b8244732226c2b03cd1a9b4690ef2774b65529e1f9cca28c5cb09191e1","0xaac9afe2f2bdbe480db4c75fc4bde587ce1da427d43fe92b351cf99a258e14b8","0xa74576670005183bbe34e18c8fcc5c1afd77bd99134ca26bc48fe29710f064e5","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[166],"10"],"treeIndex":19,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[166],"chainId":10,"proof":["0xbd102398aa029bc264af387cc8b83c3296f36263b6bc2c2b9ebf765ad8d314c0","0xf4d05e0a4bda55ba5f27d9e94590a35d5e2ca1a2faac2a9b0b41a74e841f2526","0xbea654f18382b3f24e4fc53921431a0f24b1c9a5c0eae8cf46775ae8f6aa1163","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]},{"value":["0x0000000000000000000000000000000000000009","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[496],"10"],"treeIndex":29,"claimer":"0x0000000000000000000000000000000000000009","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[496],"chainId":10,"proof":["0x052d46a76f1a529f26f38829dfae3f79e99f589af5b413d3c017ac2efabd6345","0xd714c81b0820b4076798b96e9a5abbab62fdc1b1dc7b966603e783a16f8c850c","0xa74576670005183bbe34e18c8fcc5c1afd77bd99134ca26bc48fe29710f064e5","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0x0000000000000000000000000000000000000010","24",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[14],"10"],"treeIndex":20,"claimer":"0x0000000000000000000000000000000000000010","periodId":24,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[14],"chainId":10,"proof":["0xbeda7d6d3768b215e9a1a770215b974b0cee373e6eb1ed81768df698d2758590","0xf4d05e0a4bda55ba5f27d9e94590a35d5e2ca1a2faac2a9b0b41a74e841f2526","0xbea654f18382b3f24e4fc53921431a0f24b1c9a5c0eae8cf46775ae8f6aa1163","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]},{"value":["0x0000000000000000000000000000000000000011","24",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[307],"10"],"treeIndex":22,"claimer":"0x0000000000000000000000000000000000000011","periodId":24,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[307],"chainId":10,"proof":["0x9c6b712eb6f61d531522938a8663b90b6cd18e84b5da2c988aa46e3c829169f4","0x0a6708693973ea469da5340a3b3bb4cc0544efa5e725fc976a269a8f32099e66","0xbea654f18382b3f24e4fc53921431a0f24b1c9a5c0eae8cf46775ae8f6aa1163","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]},{"value":["0x0000000000000000000000000000000000000012","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[478,561],"10"],"treeIndex":26,"claimer":"0x0000000000000000000000000000000000000012","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[478,561],"chainId":10,"proof":["0x2d7e748e5afbcf9157ebb0572295fbe19d059245281f1edd8789c61c7126608a","0xe9900a68a1549d8fec8349260de081cb229f42940c243f9ee0bf743d9b9ee28a","0x760b76a12340696370287ac0fea4e39dfe3e451f5390252891174b4f4f0a8eca","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0x0000000000000000000000000000000000000013","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[924,372],"10"],"treeIndex":30,"claimer":"0x0000000000000000000000000000000000000013","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85","0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[924,372],"chainId":10,"proof":["0x092bae9e0707af1f27496823f205004de063d11cf539111e45cae5392a119798","0xd714c81b0820b4076798b96e9a5abbab62fdc1b1dc7b966603e783a16f8c850c","0xa74576670005183bbe34e18c8fcc5c1afd77bd99134ca26bc48fe29710f064e5","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0x0000000000000000000000000000000000000014","24",["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],[76],"10"],"treeIndex":15,"claimer":"0x0000000000000000000000000000000000000014","periodId":24,"rewardTokens":["0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"],"amountsClaimed":[76],"chainId":10,"proof":["0xf6f35ef1294a1c4e09866b4e356ab39915cc6d222532cde68470c16cab4d04dc","0x65d39e0c48b9cccda676d5e49ed80af3bb2a68251c6b5de3a52205b2cb0d3334","0x337f98b7baddf65eafce39f5ac5d884ceff83d3d75e45bdb005679814d851682","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]},{"value":["0xFd35454f266dC9f672985260029f1686c6b6036c","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[533],"10"],"treeIndex":27,"claimer":"0xFd35454f266dC9f672985260029f1686c6b6036c","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[533],"chainId":10,"proof":["0x1629f910b1f9fd981142f5f9ba965d5aaa0a6dd4cd1bf6e64ceb06f3ee60fd44","0xaac9afe2f2bdbe480db4c75fc4bde587ce1da427d43fe92b351cf99a258e14b8","0xa74576670005183bbe34e18c8fcc5c1afd77bd99134ca26bc48fe29710f064e5","0x2b820a2af08d130559b84a2e5ced607b3fc71e670cf5fc94359441f107573b24"]},{"value":["0x0000000000000000000000000000000000000016","24",["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],[30],"10"],"treeIndex":18,"claimer":"0x0000000000000000000000000000000000000016","periodId":24,"rewardTokens":["0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"],"amountsClaimed":[30],"chainId":10,"proof":["0xd5b00c2f4b56531dfac15a0d3956d5adac04a628250868ba3e3d3071562c7b56","0x65b9297bb9a4b73abc45b3a24168b91bcab2cca2abd91b1512c1f06b33ef07d1","0x337f98b7baddf65eafce39f5ac5d884ceff83d3d75e45bdb005679814d851682","0x7be49e61f0211cc7082e5c7cc01a6d48dff952830be488f791759d3196226000"]}]} \ No newline at end of file