Skip to content

Superform #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 89 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
dc0a166
start deposit
sanbir Mar 17, 2025
075c086
use SingleDirectSingleVaultStateReq
sanbir Mar 19, 2025
a46b313
L2 Missing Event on cloneDeterministic Deployment
sanbir Mar 19, 2025
9b54734
L3 Missing Zero-Address Checks in Constructors
sanbir Mar 19, 2025
c60aaac
fix RPC
sanbir Mar 19, 2025
f2997f2
accept _superformCalldata
sanbir Mar 19, 2025
f770dfd
Merge branch 'ethena-audit-fixes' into superform
sanbir Mar 19, 2025
0cdbb79
check ALLOWED_DEPOSIT_SELECTORS
sanbir Mar 21, 2025
b680e1d
start testing _doWithdraw
sanbir Mar 21, 2025
75ea7c4
make compile
sanbir Mar 24, 2025
5aa1994
deposit works
sanbir Mar 24, 2025
dc14512
P2pYieldProxy__Withdrawn
sanbir Mar 24, 2025
4238849
P2pYieldProxy__Withdrawn
sanbir Mar 24, 2025
d38e07f
increaseAllowance
sanbir Mar 26, 2025
fb4a061
RunTestBase
sanbir Mar 26, 2025
b6596fa
Merge pull request #16 from sanbir/superform
sanbir Mar 27, 2025
80d08cf
run test deposit on base
sanbir Mar 27, 2025
7afd226
run test withdrawal on base
sanbir Mar 28, 2025
1531890
run test deposit on base
sanbir Mar 28, 2025
3108558
run test withdraw 2 on base
sanbir Mar 28, 2025
796ef64
test t6Mh8XAKA-ydhXRTfQmnw
sanbir Mar 28, 2025
c1c871f
test W1wJLZDxXwbebTIhv03cE
sanbir Mar 28, 2025
1fa015e
test W1wJLZDxXwbebTIhv03cE
sanbir Mar 28, 2025
d426119
fix asset
sanbir Apr 2, 2025
5a0c320
add require
sanbir Apr 2, 2025
a9b36cc
OptimismIntegration
sanbir Apr 2, 2025
a8ea946
OptimismIntegration cleanup
sanbir Apr 2, 2025
8f2d7b1
refactor AllowedCalldataChecker
sanbir Apr 2, 2025
61cd2e3
Merge branch 'p2p-org:superform' into superform
sanbir Apr 2, 2025
3ff39c3
add vaultId to log
sanbir Apr 4, 2025
a6ee54c
Merge pull request #17 from sanbir/superform
sanbir Apr 4, 2025
c5076c3
batchClaim
sanbir Apr 7, 2025
14a3090
add ClientBasisPointsOfDeposit
sanbir Apr 7, 2025
a8a38a1
fix tests
sanbir Apr 7, 2025
17d4457
test native batchClaim
sanbir Apr 9, 2025
470e3dc
test_batchclaim_proxy
sanbir Apr 9, 2025
f448d5e
Merge pull request #18 from sanbir/superform
sanbir Apr 9, 2025
6b5bf26
add _depositBatch
sanbir Apr 11, 2025
7f3a353
make compile
sanbir Apr 14, 2025
7261556
start _withdrawBatch
sanbir Apr 14, 2025
e390450
_depositBatch accept non-unique tokens
sanbir Apr 16, 2025
9d075bc
_withdrawBatch for unique assets
sanbir Apr 16, 2025
f10bac5
make compile
sanbir Apr 16, 2025
8dec01c
test OptimismDepositBatch
sanbir Apr 17, 2025
9bd42c5
rename
sanbir Apr 17, 2025
4ca5c76
P2pYieldProxy__Deposited
sanbir Apr 17, 2025
5a3cb7f
P2pYieldProxy__Withdrawn
sanbir Apr 17, 2025
699bf77
RunTestDepositBatchOptimism
sanbir Apr 17, 2025
e299421
start RunTestWithdrawBatchOptimism
sanbir Apr 17, 2025
d114393
OptimismBatchWithNative
sanbir Apr 22, 2025
4aa8014
receive ETH
sanbir Apr 22, 2025
ce2f0bd
batch deposit native
sanbir Apr 22, 2025
b2aa6b3
fix
sanbir Apr 23, 2025
ac73b1b
RunTestDepositBatchOptimism
sanbir Apr 23, 2025
4c24ae7
RunTestWithdrawBatchOptimism
sanbir Apr 23, 2025
a1caf8a
Merge pull request #19 from sanbir/superform
sanbir Apr 23, 2025
6ef9aa0
rm batch
sanbir Apr 23, 2025
87e7878
Merge pull request #20 from sanbir/superform
sanbir Apr 23, 2025
5bafd54
test native
sanbir Apr 24, 2025
da4688e
mv
sanbir Apr 24, 2025
5dbd653
fix
sanbir Apr 24, 2025
d72782d
_doDeposit native
sanbir Apr 25, 2025
19daf5b
test_happyPath_native_Optimism
sanbir Apr 25, 2025
d283cee
test_P2pOperator2Step
sanbir Apr 25, 2025
b115912
test_P2pSuperformProxy__SuperformCalldataTooShort
sanbir Apr 25, 2025
28fb053
test_P2pSuperformProxy__SelectorNotSupported
sanbir Apr 25, 2025
da5c45d
test_P2pSuperformProxy__NativeAmountToDepositAfterFeeLessThanliqReque…
sanbir Apr 25, 2025
579410d
testP2pSuperformProxy__LiqRequestTokenShouldBeEqualToPermitForP2pYiel…
sanbir Apr 25, 2025
81d653f
testP2pSuperformProxy__ShouldNotRetain4626
sanbir Apr 25, 2025
3449796
testP2pSuperformProxy__ReceiverAddressShouldBeP2pSuperformProxy
sanbir Apr 25, 2025
3862606
testP2pSuperformProxy__ReceiverAddressSPShouldBeP2pSuperformProxy
sanbir Apr 25, 2025
a663432
fix
sanbir Apr 25, 2025
2398b13
testAllowedCalldataChecker__NoAllowedCalldata
sanbir Apr 25, 2025
38085bb
testAllowedCalldataCheckerUpgrade
sanbir Apr 25, 2025
f140ad4
test vew funcs
sanbir Apr 25, 2025
17b7ed8
testP2pSuperformProxy_CheckClaim_UnauthorizedAccount
sanbir Apr 25, 2025
7e5ef6f
Merge pull request #21 from sanbir/superform
sanbir Apr 25, 2025
f4adf7c
update README.md
sanbir Apr 29, 2025
677cac6
Merge pull request #22 from sanbir/superform
sanbir Apr 29, 2025
3cb725b
fix H1
sanbir May 9, 2025
429e800
test_happyPath_Morpho_with_deposit_fee
sanbir May 12, 2025
7168afd
fix M1
sanbir May 12, 2025
6933d6b
fix L1
sanbir May 12, 2025
e1451bd
fix L2
sanbir May 12, 2025
ef3e18f
fix L3
sanbir May 12, 2025
e356e45
fix L4
sanbir May 12, 2025
ddb0a5e
fix L5
sanbir May 12, 2025
5473e70
pragma solidity 0.8.27;
sanbir May 12, 2025
b7b2a4f
Merge pull request #24 from sanbir/superform-audit-fixes
sanbir May 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 56 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -15,28 +15,28 @@ forge test
## Deployment

```shell
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
forge script script/DeployBase.s.sol:Deploy --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --chain $CHAIN_ID --json --verify --etherscan-api-key $ETHERSCAN_API_KEY -vvvvv
```

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** and **P2pSuperformProxy** contracts
- set the **P2pTreasury** address permanently in the P2pSuperformProxyFactory
- set the rules for Superform specific deposit and withdrawal functions

## 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
Expand All @@ -46,101 +46,102 @@ 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. 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:
6. Backend returns JSON to the User with (client address, client basis points, signature deadline, and the signature).

7. 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 P2pSuperformProxy instance and then from the P2pSuperformProxy instance into the Superform protocol, both of these transfers are approved by the client via Permit2. 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 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 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 do `eth_signTypedData_v4` to sign `PermitSingle` from the User's wallet into the P2pSuperformProxy instance

9. Client-side JS code prompts the User to call the `deposit` function of the P2pEthenaProxyFactory contract:
10. 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
/// @param _permitSingleForP2pYieldProxy The permit single for P2pYieldProxy
/// @param _permit2SignatureForP2pYieldProxy The permit2 signature for P2pYieldProxy
/// @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,
IAllowanceTransfer.PermitSingle memory _permitSingleForP2pYieldProxy,
bytes calldata _permit2SignatureForP2pYieldProxy,
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
Expand Down
4 changes: 3 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[profile.default]
ffi = true
fs_permissions = [{ access = "read-write", path = "./" }]
src = "src"
out = "out"
libs = ["lib"]
Expand All @@ -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" }
Binary file modified image-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 0 additions & 35 deletions script/Deploy.s.sol

This file was deleted.

51 changes: 51 additions & 0 deletions script/DeployBase.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 P2P Validator <[email protected]>
// 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,
P2pTreasury,
SuperformRouter,
SuperPositions,
address(tup),
RewardsDistributor
);

vm.stopBroadcast();

proxy = P2pSuperformProxy(payable(factory.getReferenceP2pYieldProxy()));

return (factory, proxy);
}
}

51 changes: 51 additions & 0 deletions script/DeployOptimism.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 P2P Validator <[email protected]>
// 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,
P2pTreasury,
SuperformRouter,
SuperPositions,
address(tup),
RewardsDistributor
);

vm.stopBroadcast();

proxy = P2pSuperformProxy(payable(factory.getReferenceP2pYieldProxy()));

return (factory, proxy);
}
}

Loading
Loading