Executing a Dual Investment Withdrawal: Two Approaches
Once the lock time concludes, you can withdraw your dual investment position to reclaim your tokens. There are two methods available for initiating a withdrawal, each with its own distinct advantages. The first method involves direct interaction with the Pair contract, while the second utilizes the Router contract. The primary distinction lies in the fact that the second method can facilitate the withdrawal of ETH, wherein the Router converts ETH to WETH before executing the actual withdraw. Additionally, the Router supports withdrawMultiPositions, enabling users to withdraw their positions in a single transaction. This section will provide guidance on both options.
1. Direct Withdraw within Pair Contract
You can simply call withdraw to withdraw your dual investment position and receive either one of token0 or token1.
to : Address to receive the redeemed token0 or token1.
Return Values:
token0Amt : Amount of token0 withdrawn.
token1Amt : Amount of token1 withdrawn.
Alternatively, you can withdraw a position on behalf of another address, granted approval by the respective address. In this scenario, the owner of the position needs to invoke the setApprovalForAll function to approve your address for position withdrawal. Subsequently, you can utilize the withdrawFrom function to withdraw their position on their behalf. While originally designed for the Router to manage dual investment positions on behalf of users, you can also employ this function for other purposes as necessary.
to : Address to receive the redeemed token0 or token1.
Return Value:
token0Amt : Amount of token0 withdrawn.
token1Amt : Amount of token1 withdrawn.
Set Up your Contract
Declare the solidity version used to compile the contract.
Import the IPair interface.
Write your own contract, here we name it MyWithdrawTest.
// SPDX-License-Identifier: UNLICENSEDpragmasolidity 0.8.17;interface IPair {functionwithdraw(uint index,address to) externalreturns (uint token0Amt,uint token1Amt);functionwithdrawFrom(address from,uint index,address to) externalreturns (uint token0Amts,uint token1Amts);functionsetApprovalForAll(address operator,bool approved) external;}contract MyWithdrawTest {// The DYSON-USDC pair contract on Polygon zkEVM. // In this pair, the token0 represents $DYSN and token1 represents $USDC.address dysonUsdcPair =0xC9001AFF3701e19C29E996D48e474Baf4C5eD006;// The $DYSN contract on Polygon zkEVM.addresspublicconstant DYSN =0x9CBD81b43ba263ca894178366Cfb89A246D1159C;// The $USDC contract on Polygon zkEVM.addresspublicconstant USDC =0xA8CE8aee21bC2A48a5EF670afCc9274C7bbbC035;functionwithdraw(uint index,address to) externalreturns (uint token0Amt,uint token1Amt) {returnIPair(dysonUsdcPair).withdraw(index, to); }// If pre-approved by `from`, you have the option to utilize this function to withdraw the position on their behalf.functionwithdrawFrom(address from,uint index,address to) externalreturns (uint token0Amts,uint token1Amts) {returnIPair(dysonUsdcPair).withdrawFrom(from, index, to); }}
2. Withdraw through Router Contract
Alternatively, you have the option to initiate a withdrawal through the Router contract. It's important to note that as the Router will be withdrawing the position on your behalf, you need to sign a signature and pre-approve the router for position withdrawal by calling setApprovalForAllWithSig for the pair. Details regarding the signature process will be explained later. Now, let's examine the withdrawal functions below.
to : Address that will receive either token0 or token1.
Return Value:
token0Amt : Amount of token0 withdrawn.
token1Amt : Amount of token1 withdrawn.
withdrawETH
The withdrawETH function is specifically crafted for scenarios where either token0 or token1 in the pair is WETH. This is because the Router will handle the conversion of your WETH to ETH and subsequently return it to you. If you opt for the regular withdrawal, you would receive WETH instead of ETH.
to : Address that will receive either token0 or token1 (One of them would be ETH).
Return Value:
token0Amt : Amount of token0 withdrawn.
token1Amt : Amount of token1 withdrawn.
withdrawMultiPositions
If you find yourself needing to withdraw multiple positions and prefer not to do so individually, employing the withdrawMultiPositions function is an efficient approach. This function enables you to withdraw all your positions across various pairs in a single transaction.
tos : Array of address that will receive either token0 or token1.
Return Values:
token0Amounts : Array of amount of token0 withdrawn.
token1Amounts : Array of amount of token1 withdrawn.
Approve Router for withdrawal with signature
Regardless of whether you opt for withdraw, withdrawETH, or withdrawMultiPositions, it's essential to pre-approve the Router for withdrawal on your behalf using setApprovalForAllWithSig. To illustrate, consider the following scenario: A user Alice intends to withdraw three positions with note IDs 23 and 106 in Pair 1, and note ID 14 in Pair 2. The flow chart below outlines the process:
Highlighting a crucial detail, we implement EIP-712, a standard for hashing and signing typed structured data, in our approval process. The provided code snippets below will demonstrate how to utilize the Foundry library to simulate signing that aligns with the EIP-712 standard.
Set Up your Contract
Declare the solidity version used to compile the contract.
Import IRouter interface.
This time, create your own contract by using the Foundry testing library, particularly forge-std/Test.sol. Check out the code snippets below where we make use of vm.addr() and vm.sign() to easily sign approval messages during testing.
// SPDX-License-Identifier: UNLICENSEDpragmasolidity 0.8.17;import"forge-std/Test.sol";interface IRouter {functionsetApprovalForAllWithSig(address pair,bool approved,uint deadline,bytescalldata sig) external;functionwithdraw(address pair,uint index,address to) externalreturns (uint token0Amt,uint token1Amt);functionwithdrawETH(address pair,uint index,address to) externalreturns (uint token0Amt,uint token1Amt);functionwithdrawMultiPositions(address[] calldata pairs,uint[] calldata indexes,address[] calldata tos) externalreturns (uint[] memory token0Amounts,uint[] memory token1Amounts);functionmulticall(bytes[] calldata data) externalreturns (bytes[] memory results);}interface IPair {functionnonces(address user) externalviewreturns (uint);}contractMyWithdrawTestisTest {// The Router contract on Polygon zkEVM.addresspublicconstant router =0xADa6e69781399990d42bEcB1a9427955FFA73Bdc;// EIP712: TYPEHASH for signing for setApprovalForAllWithSig.bytes32constant APPROVE_TYPEHASH =keccak256("setApprovalForAllWithSig(address owner,address operator,bool approved,uint256 nonce,uint256 deadline)");// Alice's wallet private keyuint aliceKey =123456;// Alice's wallet addressaddress alice =0x1234....;uint deadline = block.timestamp +1;functionwithdraw(address pair,uint index) externalreturns (uint token0Amt,uint token1Amt) {bytesmemory sig =_getApprovalSig(pair, aliceKey,true, deadline);IRouter(router).setApprovalForAllWithSig(pair,true, deadline, sig); (token0Amt, token1Amt) =IRouter(router).withdraw(pair, index, alice); }functionwithdrawETH(address pair,uint index) externalreturns (uint token0Amt,uint token1Amt) {bytesmemory sig =_getApprovalSig(pair, aliceKey,true, deadline);IRouter(router).setApprovalForAllWithSig(pair,true, deadline, sig); (token0Amt, token1Amt) =IRouter(router).withdrawETH(pair, index, alice); }// Just like the scenario above, Alice intends to withdraw three positions // with note IDs 23 and 106 in Pair 1, and note ID 14 in Pair 2.addresspublic pair1 =0x5678....;addresspublic pair2 =0x6789....;functionwithdrawMultiPositions() externalreturns (uint[] memory token0Amounts,uint[] memory token1Amounts) {bytesmemory sig =_getApprovalSig(pair1, aliceKey,true, deadline);bytesmemory sig2 =_getApprovalSig(pair2, aliceKey,true, deadline);bytes[] memory data =newbytes[](2); data[0] = abi.encodeWithSelector(IRouter.setApprovalForAllWithSig.selector, pair1,true, deadline, sig); data[1] = abi.encodeWithSelector(IRouter.setApprovalForAllWithSig.selector, pair2,true, deadline, sig2);// Use multical to set approval for all poolsIRouter(router).multicall(data); address[] memory pairs =newaddress[](3); pairs[0] = pair1; pairs[1] = pair1; pairs[2] = pair2;uint[] memory indexes =newuint[](3); indexes[0] =23; indexes[1] =106; indexes[2] =14;address[] memory tos =newaddress[](4);for(uint i=0; i <3; i++) { tos[i] = alice; } (token0Amounts, token1Amounts) =IRouter(router).withdrawMultiPositions(pairs, indexes, tos); }// Internal function for signing an approval.function_getApprovalSig(address pair,uint fromKey,bool approved,uint_deadline) privateviewreturns (bytesmemory) {address fromAddr = vm.addr(fromKey);bytes32 structHash =keccak256(abi.encode(APPROVE_TYPEHASH, fromAddr,address(router), approved,IPair(pair).nonces(fromAddr), _deadline));bytes32 digest =keccak256(abi.encodePacked("\x19\x01",_getPairDomainSeparator(pair), structHash)); (uint8 v,bytes32 r,bytes32 s) = vm.sign(fromKey, digest);return abi.encodePacked(r, s, v); }// Internal function for acquiring domain separator.function_getPairDomainSeparator(address pair) privateviewreturns (bytes32) {returnkeccak256( abi.encode(keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),keccak256(bytes("Pair")),keccak256(bytes('1')), block.chainid, pair ) ); }}