Contracts for depositing and withdrawing ERC-20 tokens from yield protocols. The current implementation is only compatible with Ethena protocol.
curl -L https://foundry.paradigm.xyz | bash
source /Users/$USER/.bashrc
foundryup
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
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
Look at function _doDeposit() for a reference implementation of the flow.
-
Website User (called Client in contracts) calls Backend with its (User's) Ethereum address and some Merchant info.
-
Backend uses Merchant info to determine the P2P fee (expressed as client basis points in the contracts).
-
Backend calls P2pEthenaProxyFactory's
getHashForP2pSigner
function to get the hash for the P2pSigner.
/// @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
function getHashForP2pSigner(
address _client,
uint96 _clientBasisPoints,
uint256 _p2pSignerSigDeadline
) external view returns (bytes32);
-
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. -
Backend returns JSON to the User with (client address, client basis points, signature deadline, and the signature).
-
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:
/// @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
/// @param _client The address of client
/// @return address The address of the P2pEthenaProxy instance
function predictP2pEthenaProxyAddress(
address _client,
uint96 _clientBasisPoints
) external view returns (address);
-
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. -
Client-side JS code prompts the User to do
eth_signTypedData_v4
to signPermitSingle
from the User's wallet into the P2pEthenaProxy instance -
Client-side JS code prompts the User to call the
deposit
function of the P2pEthenaProxyFactory contract:
/// @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 _p2pSignerSigDeadline The P2pSigner signature deadline
/// @param _p2pSignerSignature The P2pSigner signature
/// @return P2pEthenaProxyAddress The client's P2pEthenaProxy instance address
function deposit(
IAllowanceTransfer.PermitSingle memory _permitSingleForP2pEthenaProxy,
bytes calldata _permit2SignatureForP2pEthenaProxy,
uint96 _clientBasisPoints,
uint256 _p2pSignerSigDeadline,
bytes calldata _p2pSignerSignature
)
external
returns (address P2pEthenaProxyAddress);
Look at function _doWithdraw() for a reference implementation of the flow.
-
Client-side JS code prepares all the necessary data for the Ethena
cooldownShares
function. -
Client-side JS code prompts the User to call the
cooldownShares
function of the client's instance of the P2pEthenaProxy contract:
/// @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);
-
Wait for the cooldownDuration. (Currently, 7 days).
-
Client-side JS code prompts the User to call the
withdrawAfterCooldown
function of the client's instance of the P2pEthenaProxy contract:
/// @notice withdraw assets after cooldown has elapsed
function withdrawAfterCooldown() 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.
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.
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:
/// @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;
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:
/// @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;