@dhedge/v2-sdk 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,19 +70,23 @@ const getSwapWithdrawData = async (
70
70
  }
71
71
  throw new Error("All swap routers failed for complete withdrawal");
72
72
  };
73
+
73
74
  export const createCompleteWithdrawalTxArguments = async (
74
75
  pool: Pool,
75
76
  receiveToken: string,
76
- slippage: number
77
+ slippage: number,
78
+ _trackedAssets: TrackedAsset[]
77
79
  ): Promise<any> => {
78
80
  const easySwapper = new ethers.Contract(
79
81
  routerAddress[pool.network][Dapp.TOROS] as string,
80
82
  IEasySwapperV2,
81
83
  pool.signer
82
84
  );
83
- const trackedAssets: TrackedAsset[] = await easySwapper.getTrackedAssets(
84
- pool.address
85
- );
85
+
86
+ let trackedAssets: TrackedAsset[] = _trackedAssets;
87
+ if (trackedAssets.length === 0) {
88
+ trackedAssets = await easySwapper.getTrackedAssets(pool.address);
89
+ }
86
90
 
87
91
  if (
88
92
  trackedAssets.length === 0 ||
@@ -170,10 +174,20 @@ export const createCompleteWithdrawalTxArguments = async (
170
174
  const withdrawalVaultAddress = await easySwapper.withdrawalContracts(
171
175
  pool.address
172
176
  );
173
- const balanceOfReceiveToken = await receiveTokenErc20.balanceOf(
177
+ let balanceOfReceiveToken = await receiveTokenErc20.balanceOf(
174
178
  withdrawalVaultAddress
175
179
  );
176
180
 
181
+ if (trackedAssets.length !== 0) {
182
+ //  finds the receiveTokenErc20's balance inside trackedAssets
183
+ const trackedAsset = trackedAssets.find(
184
+ ({ token }) => token.toLowerCase() === receiveToken.toLowerCase()
185
+ );
186
+ if (trackedAsset) {
187
+ balanceOfReceiveToken = balanceOfReceiveToken.add(trackedAsset.balance);
188
+ }
189
+ }
190
+
177
191
  // complete withdraw _expectedDestTokenAmount
178
192
  const estimatedMinReceiveAmount = swapDestMinDestAmount.plus(
179
193
  balanceOfReceiveToken.toString()
@@ -201,12 +215,14 @@ export const getCompleteWithdrawalTxData = async (
201
215
  pool: Pool,
202
216
  receiveToken: string,
203
217
  slippage: number,
204
- useOnChainSwap: boolean
218
+ useOnChainSwap: boolean,
219
+ trackedAssets: TrackedAsset[]
205
220
  ): Promise<string> => {
206
221
  const completeWithdrawTxArguments = await createCompleteWithdrawalTxArguments(
207
222
  pool,
208
223
  receiveToken,
209
- slippage
224
+ slippage,
225
+ trackedAssets
210
226
  );
211
227
 
212
228
  const isSwapNeeded = completeWithdrawTxArguments.isSwapNeeded;
@@ -70,7 +70,7 @@ export async function getEasySwapperTxData(
70
70
  amountIn: ethers.BigNumber,
71
71
  slippage: number
72
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
- ): Promise<any> {
73
+ ): Promise<{ swapTxData: string; minAmountOut?: any }> {
74
74
  const isWithdrawal = await isPool(pool, assetFrom);
75
75
  const [torosAsset, investAsset] = isWithdrawal
76
76
  ? [assetFrom, assetTo]
@@ -99,11 +99,13 @@ export async function getEasySwapperTxData(
99
99
  investAsset,
100
100
  amountIn
101
101
  );
102
- return iEasySwapperV2.encodeFunctionData("depositWithCustomCooldown", [
103
- torosAsset,
104
- depositAsset,
105
- amountIn,
106
- minAmountOut.mul(10000 - slippage * 100).div(10000)
107
- ]);
102
+ const _minAmountOut = minAmountOut.mul(10000 - slippage * 100).div(10000);
103
+ return {
104
+ swapTxData: iEasySwapperV2.encodeFunctionData(
105
+ "depositWithCustomCooldown",
106
+ [torosAsset, depositAsset, amountIn, _minAmountOut]
107
+ ),
108
+ minAmountOut: _minAmountOut
109
+ };
108
110
  }
109
111
  }
@@ -157,7 +157,7 @@ export const getInitWithdrawalTxData = async (
157
157
  amountIn: string,
158
158
  slippage: number,
159
159
  useOnChainSwap: boolean
160
- ): Promise<string> => {
160
+ ): Promise<{ swapTxData: string; minAmountOut?: any }> => {
161
161
  const complexAssetsData = await createWithdrawTxArguments(
162
162
  pool,
163
163
  torosAsset,
@@ -166,9 +166,13 @@ export const getInitWithdrawalTxData = async (
166
166
  useOnChainSwap
167
167
  );
168
168
  const iEasySwapperV2 = new ethers.utils.Interface(IEasySwapperV2);
169
- return iEasySwapperV2.encodeFunctionData("initWithdrawal", [
170
- torosAsset,
171
- amountIn,
172
- complexAssetsData
173
- ]);
169
+
170
+ return {
171
+ swapTxData: iEasySwapperV2.encodeFunctionData("initWithdrawal", [
172
+ torosAsset,
173
+ amountIn,
174
+ complexAssetsData
175
+ ]),
176
+ minAmountOut: null // not be used when building multicall tx data
177
+ };
174
178
  };
@@ -0,0 +1,241 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+
3
+ import BigNumber from "bignumber.js";
4
+ import { BigNumber as EthersBigNumber, Contract, providers } from "ethers";
5
+ import { Dhedge, Pool } from "..";
6
+ import { Dapp, Network } from "../types";
7
+ import { CONTRACT_ADDRESS, MAX_AMOUNT } from "./constants";
8
+ import {
9
+ TestingRunParams,
10
+ runWithImpersonateAccount,
11
+ setTokenAmount,
12
+ setUSDCAmount,
13
+ testingHelper
14
+ } from "./utils/testingHelper";
15
+ import { balanceDelta } from "./utils/token";
16
+ import { routerAddress } from "../config";
17
+
18
+ const officeAbi = [
19
+ "function getMarketConfig(uint88 market) view returns (address marketConfig)"
20
+ ];
21
+ const marketConfigAbi = ["function hooks() view returns (address hooks)"];
22
+ const borrowerWhitelistAbi = [
23
+ "function owner() view returns (address)",
24
+ "function setAddressWhitelist(address accountOwner, bool allowed)"
25
+ ];
26
+ const erc20AllowanceAbi = [
27
+ "function allowance(address owner, address spender) view returns (uint256)"
28
+ ];
29
+
30
+ const DYTM_MARKET_ID = 1;
31
+ // ReserveKey: (marketId uint88 << 160) | asset address — DYTM src/types/ReserveKey.sol
32
+ const toReserveKey = (marketId: number, asset: string): EthersBigNumber =>
33
+ EthersBigNumber.from(marketId)
34
+ .shl(160)
35
+ .or(asset);
36
+ // full tokenId: (TokenType << 248) | reserveKey — DYTM src/libraries/TokenHelpers.sol
37
+ // TokenType: ESCROW = 1, LEND = 2, DEBT = 3
38
+ const toLentId = (key: EthersBigNumber): EthersBigNumber =>
39
+ EthersBigNumber.from(2)
40
+ .shl(248)
41
+ .or(key);
42
+ const toEscrowId = (key: EthersBigNumber): EthersBigNumber =>
43
+ EthersBigNumber.from(1)
44
+ .shl(248)
45
+ .or(key);
46
+
47
+ // SPYon (Ondo) collateral on mainnet market 1
48
+ const SPYON_MAINNET = "0xFeDC5f4a6c38211c1338aa411018DFAf26612c08";
49
+ const SPYON_BALANCEOF_SLOT = 51; // OZ ERC20Upgradeable layout
50
+
51
+ // market 1 borrows are gated by the BorrowerWhitelist hook — impersonate its
52
+ // owner and whitelist the pool
53
+ const whitelistPoolAsBorrower = async ({
54
+ pool,
55
+ network,
56
+ provider
57
+ }: {
58
+ pool: Pool;
59
+ network: Network;
60
+ provider: providers.JsonRpcProvider;
61
+ }): Promise<void> => {
62
+ const office = new Contract(
63
+ routerAddress[network][Dapp.DYTM]!,
64
+ officeAbi,
65
+ provider
66
+ );
67
+ const marketConfigAddress = await office.getMarketConfig(DYTM_MARKET_ID);
68
+ const hookAddress = await new Contract(
69
+ marketConfigAddress,
70
+ marketConfigAbi,
71
+ provider
72
+ ).hooks();
73
+ const hook = new Contract(hookAddress, borrowerWhitelistAbi, provider);
74
+ const hookOwner = await hook.owner();
75
+ await runWithImpersonateAccount(
76
+ { account: hookOwner, provider },
77
+ async ({ signer }) => {
78
+ await hook.connect(signer).setAddressWhitelist(pool.address, true);
79
+ }
80
+ );
81
+ };
82
+
83
+ const testDytm = ({ network, wallet, provider }: TestingRunParams) => {
84
+ const USDC = CONTRACT_ADDRESS[network].USDC;
85
+ const usdcKey = toReserveKey(DYTM_MARKET_ID, USDC);
86
+ // supply/withdraw take the full LEND tokenId; borrow/repay take the bare ReserveKey
87
+ const USDC_DEPOSIT_TOKEN = toLentId(usdcKey).toString();
88
+ const USDC_BORROW_TOKEN = usdcKey.toString();
89
+
90
+ const USDC_FUNDING_AMOUNT = new BigNumber(100).shiftedBy(6).toFixed(0);
91
+ const USDC_LIQUIDITY_AMOUNT = new BigNumber(50).shiftedBy(6).toFixed(0);
92
+
93
+ // the asset backing the borrow differs per network: on arbitrum lent USDC
94
+ // counts as collateral, on mainnet USDC has lend weight 0 and SPYon escrow
95
+ // collateral (weight 0.75) is required instead
96
+ const collateral =
97
+ network === Network.ETHEREUM
98
+ ? {
99
+ asset: SPYON_MAINNET,
100
+ tokenId: toEscrowId(
101
+ toReserveKey(DYTM_MARKET_ID, SPYON_MAINNET)
102
+ ).toString(),
103
+ supplyAmount: new BigNumber(1).shiftedBy(18).toFixed(0),
104
+ // SPYon isn't covered by the USDC funding — deal it via its balanceOf slot
105
+ funding: {
106
+ slot: SPYON_BALANCEOF_SLOT,
107
+ amount: new BigNumber(10).shiftedBy(18).toFixed(0)
108
+ }
109
+ }
110
+ : {
111
+ asset: USDC,
112
+ tokenId: USDC_DEPOSIT_TOKEN,
113
+ supplyAmount: USDC_LIQUIDITY_AMOUNT,
114
+ funding: null // covered by the USDC funded in beforeAll
115
+ };
116
+
117
+ let dhedge: Dhedge;
118
+ let pool: Pool;
119
+ jest.setTimeout(100000);
120
+
121
+ describe(`[${network}] DYTM tests`, () => {
122
+ beforeAll(async () => {
123
+ // top up ETH (gas)
124
+ await provider.send("hardhat_setBalance", [
125
+ wallet.address,
126
+ "0x100000000000000"
127
+ ]);
128
+ // mine a local block so eth_calls run under hardhat's own hardfork —
129
+ // the remote fork-point block executes as london (hardforkHistory) and
130
+ // lacks the cancun opcodes DYTM needs (transient storage)
131
+ await provider.send("evm_mine", []);
132
+ dhedge = new Dhedge(wallet, network);
133
+ pool = await dhedge.loadPool(wallet.address, false);
134
+
135
+ await whitelistPoolAsBorrower({ pool, network, provider });
136
+
137
+ // fund USDC and supply half of it as market liquidity to be borrowed
138
+ await setUSDCAmount({
139
+ amount: USDC_FUNDING_AMOUNT,
140
+ userAddress: pool.address,
141
+ network,
142
+ provider
143
+ });
144
+ await pool.approve(Dapp.DYTM, USDC, MAX_AMOUNT);
145
+ await pool.lend(Dapp.DYTM, USDC_DEPOSIT_TOKEN, USDC_LIQUIDITY_AMOUNT);
146
+
147
+ // fund the collateral asset for the supply tests
148
+ if (collateral.funding) {
149
+ await setTokenAmount({
150
+ amount: collateral.funding.amount,
151
+ userAddress: pool.address,
152
+ tokenAddress: collateral.asset,
153
+ slot: collateral.funding.slot,
154
+ provider
155
+ });
156
+ }
157
+ });
158
+
159
+ it("approves unlimited collateral for DYTM", async () => {
160
+ await pool.approve(Dapp.DYTM, collateral.asset, MAX_AMOUNT);
161
+ const allowance = await new Contract(
162
+ collateral.asset,
163
+ erc20AllowanceAbi,
164
+ provider
165
+ ).allowance(pool.address, routerAddress[network][Dapp.DYTM]!);
166
+ expect(allowance.eq(MAX_AMOUNT)).toBe(true);
167
+ });
168
+
169
+ it("estimates supplying collateral to DYTM market", async () => {
170
+ const result = await pool.lend(
171
+ Dapp.DYTM,
172
+ collateral.tokenId,
173
+ collateral.supplyAmount,
174
+ 0,
175
+ null,
176
+ true
177
+ );
178
+ expect(result.gas.gt(0)).toBe(true);
179
+ });
180
+
181
+ it("supplies collateral to DYTM market", async () => {
182
+ await pool.lend(Dapp.DYTM, collateral.tokenId, collateral.supplyAmount);
183
+
184
+ const collateralDelta = await balanceDelta(
185
+ pool.address,
186
+ collateral.asset,
187
+ pool.signer
188
+ );
189
+ expect(collateralDelta.lt(0)).toBe(true);
190
+ });
191
+
192
+ it("borrows USDC from DYTM market", async () => {
193
+ // borrow value must clear the market's minDebtAmountUSD ($1 on arbitrum);
194
+ // borrow 3 USDC so the ~2 USDC debt left after the repay test stays above it
195
+ await pool.borrow(Dapp.DYTM, USDC_BORROW_TOKEN, (3 * 1e6).toString());
196
+
197
+ const usdcTokenDelta = await balanceDelta(
198
+ pool.address,
199
+ USDC,
200
+ pool.signer
201
+ );
202
+ expect(usdcTokenDelta.gt(0)).toBe(true);
203
+ });
204
+
205
+ it("repays USDC to DYTM market", async () => {
206
+ await pool.repay(Dapp.DYTM, USDC_BORROW_TOKEN, (1 * 1e6).toString());
207
+
208
+ const usdcTokenDelta = await balanceDelta(
209
+ pool.address,
210
+ USDC,
211
+ pool.signer
212
+ );
213
+ expect(usdcTokenDelta.lt(0)).toBe(true);
214
+ });
215
+
216
+ it("withdraws USDC from DYTM market", async () => {
217
+ await pool.withdrawDeposit(
218
+ Dapp.DYTM,
219
+ USDC_DEPOSIT_TOKEN,
220
+ (5 * 1e6).toString()
221
+ );
222
+
223
+ const usdcTokenDelta = await balanceDelta(
224
+ pool.address,
225
+ USDC,
226
+ pool.signer
227
+ );
228
+ expect(usdcTokenDelta.gt(0)).toBe(true);
229
+ });
230
+ });
231
+ };
232
+
233
+ testingHelper({
234
+ network: Network.ARBITRUM,
235
+ testingRun: testDytm
236
+ });
237
+
238
+ testingHelper({
239
+ network: Network.ETHEREUM,
240
+ testingRun: testDytm
241
+ });
@@ -0,0 +1,132 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+
3
+ /**
4
+ * Ondo Global Markets on-chain tests for minting and redeeming GM tokens.
5
+ * These require a live chain connection (onFork: false) because each mint/redeem
6
+ * needs a fresh attestation signed by the Ondo API against current chain state,
7
+ * which cannot be reproduced on a Hardhat fork.
8
+ *
9
+ * Prerequisites:
10
+ * - PRIVATE_KEY in .env (must be the pool manager or trader)
11
+ * - ETHEREUM_URL in .env
12
+ * - ONDO_API_KEY in .env
13
+ * - The test pool must hold USDC to mint (and SPYon, from the mint, to redeem)
14
+ */
15
+
16
+ import { ethers } from "ethers";
17
+ import { Dhedge, Pool } from "..";
18
+
19
+ import { Dapp, Network } from "../types";
20
+ import { CONTRACT_ADDRESS, MAX_AMOUNT } from "./constants";
21
+ import { TestingRunParams, testingHelper } from "./utils/testingHelper";
22
+
23
+ import { getTxOptions } from "./txOptions";
24
+ import { balanceDelta } from "./utils/token";
25
+
26
+ import { routerAddress } from "../config";
27
+
28
+ const testOndo = ({ wallet, network }: TestingRunParams) => {
29
+ const USDC = CONTRACT_ADDRESS[network].USDC;
30
+ const SPYon = "0xfedc5f4a6c38211c1338aa411018dfaf26612c08";
31
+
32
+ let dhedge: Dhedge;
33
+ let pool: Pool;
34
+ jest.setTimeout(100000);
35
+
36
+ describe(`pool on ${network}`, () => {
37
+ beforeAll(async () => {
38
+ dhedge = new Dhedge(wallet, network);
39
+ pool = await dhedge.loadPool(
40
+ "0x9f647b85A514b1e60F8E8E956E636a50dA406279"
41
+ );
42
+ });
43
+
44
+ it("approves unlimited USDC on Ondo", async () => {
45
+ const tx = await pool.approve(Dapp.ONDO, USDC, MAX_AMOUNT);
46
+ await tx.wait();
47
+ const iERC20 = new ethers.Contract(
48
+ USDC,
49
+ ["function allowance(address,address) view returns (uint256)"],
50
+ pool.signer
51
+ );
52
+ const allowance = await iERC20.allowance(
53
+ pool.address,
54
+ routerAddress[network][Dapp.ONDO]!
55
+ );
56
+ expect(allowance.gt(0)).toBe(true);
57
+ });
58
+
59
+ it("gets gas estimation for 40 USDC into SPYon on Ondo", async () => {
60
+ const gasEstimate = await pool.trade(
61
+ Dapp.ONDO,
62
+ USDC,
63
+ SPYon,
64
+ "40000000",
65
+ 0.1,
66
+ await getTxOptions(network),
67
+ true
68
+ );
69
+ expect(gasEstimate.gasEstimationError).toBeNull();
70
+ expect(gasEstimate.gas.gt(0)).toBe(true);
71
+ expect(gasEstimate.minAmountOut).not.toBeNull();
72
+ });
73
+
74
+ it("trades 40 USDC into SPYon on Ondo", async () => {
75
+ const tx = await pool.trade(
76
+ Dapp.ONDO,
77
+ USDC,
78
+ SPYon,
79
+ "40000000",
80
+ 0.1,
81
+ await getTxOptions(network)
82
+ );
83
+ await tx.wait();
84
+ const spBalanceDelta = await balanceDelta(
85
+ pool.address,
86
+ SPYon,
87
+ pool.signer
88
+ );
89
+ expect(spBalanceDelta.gt(0)).toBe(true);
90
+ });
91
+
92
+ it("approves unlimited SPYon on Ondo", async () => {
93
+ const tx = await pool.approve(Dapp.ONDO, SPYon, MAX_AMOUNT);
94
+ await tx.wait();
95
+ const iERC20 = new ethers.Contract(
96
+ SPYon,
97
+ ["function allowance(address,address) view returns (uint256)"],
98
+ pool.signer
99
+ );
100
+ const allowance = await iERC20.allowance(
101
+ pool.address,
102
+ routerAddress[network][Dapp.ONDO]!
103
+ );
104
+ expect(allowance.gt(0)).toBe(true);
105
+ });
106
+
107
+ it("sells SPYon balance on Ondo", async () => {
108
+ const spyonBalance = await pool.utils.getBalance(SPYon, pool.address);
109
+ const tx = await pool.trade(
110
+ Dapp.ONDO,
111
+ SPYon,
112
+ USDC,
113
+ spyonBalance,
114
+ 0.1,
115
+ await getTxOptions(network)
116
+ );
117
+ await tx.wait();
118
+ const usdcBalanceDelta = await balanceDelta(
119
+ pool.address,
120
+ USDC,
121
+ pool.signer
122
+ );
123
+ expect(usdcBalanceDelta.gt(0)).toBe(true);
124
+ });
125
+ });
126
+ };
127
+
128
+ testingHelper({
129
+ network: Network.ETHEREUM,
130
+ testingRun: testOndo,
131
+ onFork: false
132
+ });
package/src/types.ts CHANGED
@@ -36,8 +36,10 @@ export enum Dapp {
36
36
  ODOS = "odos",
37
37
  PENDLE = "pendle",
38
38
  KYBERSWAP = "kyberswap",
39
+ DYTM = "dytm",
39
40
  HYPERLIQUID = "hyperliquid",
40
- COWSWAP = "cowswap"
41
+ COWSWAP = "cowswap",
42
+ ONDO = "ondo"
41
43
  }
42
44
 
43
45
  /** Function-name strings used when encoding ABI calls — keep in sync with the