Skip to content

Commit 97a9add

Browse files
fix: borrow within isolated e-mode groups (#5582)
chore: bump package versions Co-authored-by: toolsvenus <tools@venus.io>
1 parent e1f1db9 commit 97a9add

4 files changed

Lines changed: 159 additions & 100 deletions

File tree

.changeset/hungry-tips-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@venusprotocol/evm": minor
3+
---
4+
5+
fix issue with borrow capability incorrectly detected for isolated e-mode groups

apps/evm/src/containers/AssetAccessor/__tests__/__snapshots__/index.spec.tsx.snap

Lines changed: 0 additions & 3 deletions
This file was deleted.

apps/evm/src/containers/AssetAccessor/__tests__/index.spec.tsx

Lines changed: 143 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,91 +3,107 @@ import type { Mock } from 'vitest';
33

44
import fakeAddress from '__mocks__/models/address';
55
import { poolData } from '__mocks__/models/pools';
6-
import { renderComponent } from 'testUtils/render';
7-
86
import { useGetPool } from 'clients/api';
97
import { TokenAnnouncement } from 'containers/TokenAnnouncement';
108
import { en } from 'libs/translations';
9+
import { renderComponent } from 'testUtils/render';
1110
import type { Asset, Pool } from 'types';
1211

1312
import AssetAccessor, { type AssetAccessorProps } from '..';
1413

1514
vi.mock('containers/TokenAnnouncement');
1615

16+
const mockUseGetPool = useGetPool as Mock;
17+
const mockTokenAnnouncement = TokenAnnouncement as Mock;
18+
1719
const fakePool = poolData[0];
1820
const fakeAsset = fakePool.assets[0];
1921

20-
const fakeProps: Omit<AssetAccessorProps, 'children'> = {
22+
const borrowProps: Omit<AssetAccessorProps, 'children'> = {
2123
poolComptrollerAddress: fakePool.comptrollerAddress,
2224
vToken: fakeAsset.vToken,
2325
action: 'borrow',
2426
};
2527

28+
const supplyProps: Omit<AssetAccessorProps, 'children'> = {
29+
...borrowProps,
30+
action: 'supply',
31+
};
32+
2633
const fakeChildrenContent = 'Fake content';
2734

2835
const TestComponent = () => <>{fakeChildrenContent}</>;
2936

37+
const getCustomFakePool = ({
38+
pool = fakePool,
39+
asset = {},
40+
}: {
41+
pool?: Pool;
42+
asset?: Partial<Asset>;
43+
} = {}): Pool => ({
44+
...pool,
45+
assets: pool.assets.map(currentAsset =>
46+
currentAsset.vToken.address === fakeAsset.vToken.address
47+
? {
48+
...currentAsset,
49+
...asset,
50+
}
51+
: currentAsset,
52+
),
53+
});
54+
55+
const mockGetPool = (pool: Pool = fakePool) => {
56+
mockUseGetPool.mockImplementation(() => ({
57+
isLoading: false,
58+
data: {
59+
pool,
60+
},
61+
}));
62+
};
63+
3064
describe('containers/AssetAccessor', () => {
3165
it('renders without crashing', async () => {
32-
renderComponent(<AssetAccessor {...fakeProps}>{() => <TestComponent />}</AssetAccessor>);
33-
});
66+
const { getByText } = renderComponent(
67+
<AssetAccessor {...borrowProps}>{() => <TestComponent />}</AssetAccessor>,
68+
);
3469

35-
it('renders token announcement if action has been disabled and an announcement exists for that token', async () => {
36-
const customFakePool: Pool = {
37-
...fakePool,
38-
assets: fakePool.assets.map(asset =>
39-
asset.vToken.address === fakeAsset.vToken.address
40-
? {
41-
...asset,
42-
disabledTokenActions: ['borrow'],
43-
}
44-
: asset,
45-
),
46-
};
70+
await waitFor(() => expect(getByText(fakeChildrenContent)).toBeInTheDocument());
71+
});
4772

48-
(useGetPool as Mock).mockImplementation(() => ({
49-
isLoading: false,
50-
data: {
51-
pool: customFakePool,
52-
},
53-
}));
73+
it('renders token announcement if a disabled borrow action has an announcement for the token', async () => {
74+
mockGetPool(
75+
getCustomFakePool({
76+
asset: {
77+
disabledTokenActions: ['borrow'],
78+
},
79+
}),
80+
);
5481

5582
const fakeTokenAnnouncementText = 'Fake token announcement';
56-
(TokenAnnouncement as Mock).mockImplementation(() => fakeTokenAnnouncementText);
83+
mockTokenAnnouncement.mockImplementation(() => fakeTokenAnnouncementText);
5784

5885
const { getByText, queryByText } = renderComponent(
59-
<AssetAccessor {...fakeProps}>{() => <TestComponent />}</AssetAccessor>,
86+
<AssetAccessor {...borrowProps}>{() => <TestComponent />}</AssetAccessor>,
6087
);
6188

6289
await waitFor(() => expect(getByText(fakeTokenAnnouncementText)).toBeInTheDocument());
6390

6491
expect(queryByText(fakeChildrenContent)).toBeNull();
6592
});
6693

67-
it('renders default token announcement if action has been disabled and no announcement exists for that token', async () => {
68-
const customFakePool: Pool = {
69-
...fakePool,
70-
assets: fakePool.assets.map(asset =>
71-
asset.vToken.address === fakeAsset.vToken.address
72-
? {
73-
...asset,
74-
disabledTokenActions: ['borrow'],
75-
}
76-
: asset,
77-
),
78-
};
79-
80-
(useGetPool as Mock).mockImplementation(() => ({
81-
isLoading: false,
82-
data: {
83-
pool: customFakePool,
84-
},
85-
}));
94+
it('renders default token announcement if a disabled borrow action has no token announcement', async () => {
95+
mockGetPool(
96+
getCustomFakePool({
97+
asset: {
98+
disabledTokenActions: ['borrow'],
99+
},
100+
}),
101+
);
86102

87-
(TokenAnnouncement as Mock).mockImplementation(() => null);
103+
mockTokenAnnouncement.mockImplementation(() => null);
88104

89105
const { getByText, queryByText } = renderComponent(
90-
<AssetAccessor {...fakeProps}>{() => <TestComponent />}</AssetAccessor>,
106+
<AssetAccessor {...borrowProps}>{() => <TestComponent />}</AssetAccessor>,
91107
);
92108

93109
await waitFor(() =>
@@ -97,30 +113,41 @@ describe('containers/AssetAccessor', () => {
97113
expect(queryByText(fakeChildrenContent)).toBeNull();
98114
});
99115

100-
it('renders default announcement about borrow being disabled if asset is not borrowable', async () => {
101-
const customFakePool: Pool = {
102-
...fakePool,
103-
assets: fakePool.assets.map(asset =>
104-
asset.vToken.address === fakeAsset.vToken.address
105-
? {
106-
...asset,
107-
isBorrowable: false,
108-
}
109-
: asset,
110-
),
111-
};
116+
it('renders token announcement if a disabled non-borrow action has an announcement for the token', async () => {
117+
mockGetPool(
118+
getCustomFakePool({
119+
asset: {
120+
disabledTokenActions: ['supply'],
121+
},
122+
}),
123+
);
112124

113-
(useGetPool as Mock).mockImplementation(() => ({
114-
isLoading: false,
115-
data: {
116-
pool: customFakePool,
117-
},
118-
}));
125+
const fakeTokenAnnouncementText = 'Fake token announcement';
126+
mockTokenAnnouncement.mockImplementation(() => fakeTokenAnnouncementText);
127+
128+
const { getByText, queryByText } = renderComponent(
129+
<AssetAccessor {...supplyProps}>{() => <TestComponent />}</AssetAccessor>,
130+
);
131+
132+
await waitFor(() => expect(getByText(fakeTokenAnnouncementText)).toBeInTheDocument());
119133

120-
(TokenAnnouncement as Mock).mockImplementation(() => null);
134+
expect(queryByText(fakeChildrenContent)).toBeNull();
135+
});
136+
137+
it('renders default announcement about borrow being disabled if the asset is not borrowable from the pool', async () => {
138+
mockGetPool(
139+
getCustomFakePool({
140+
asset: {
141+
isBorrowable: false,
142+
isBorrowableByUser: false,
143+
},
144+
}),
145+
);
146+
147+
mockTokenAnnouncement.mockImplementation(() => null);
121148

122149
const { getByText, queryByText } = renderComponent(
123-
<AssetAccessor {...fakeProps}>{() => <TestComponent />}</AssetAccessor>,
150+
<AssetAccessor {...borrowProps}>{() => <TestComponent />}</AssetAccessor>,
124151
);
125152

126153
await waitFor(() =>
@@ -130,34 +157,57 @@ describe('containers/AssetAccessor', () => {
130157
expect(queryByText(fakeChildrenContent)).toBeNull();
131158
});
132159

133-
it('renders default announcement about borrow being disabled due to E-mode settings if asset is not borrowable by user', async () => {
134-
const customFakePool: Pool = {
135-
...fakePool,
136-
assets: fakePool.assets.map(asset =>
137-
asset.vToken.address === fakeAsset.vToken.address
138-
? {
139-
...asset,
140-
isBorrowableByUser: false,
141-
}
142-
: asset,
160+
it('renders default announcement about borrow being disabled due to E-mode settings if the asset is not borrowable by the user', async () => {
161+
mockGetPool(
162+
getCustomFakePool({
163+
pool: {
164+
...fakePool,
165+
userEModeGroup: fakePool.eModeGroups[0],
166+
},
167+
asset: {
168+
isBorrowableByUser: false,
169+
},
170+
}),
171+
);
172+
173+
const { container, getByText, queryByText } = renderComponent(
174+
<AssetAccessor {...borrowProps}>{() => <TestComponent />}</AssetAccessor>,
175+
);
176+
177+
await waitFor(() =>
178+
expect(container).toHaveTextContent(
179+
'Your E-mode settings do not allow you to borrow from this market.',
143180
),
144-
};
181+
);
145182

146-
(useGetPool as Mock).mockImplementation(() => ({
147-
isLoading: false,
148-
data: {
149-
pool: customFakePool,
150-
},
151-
}));
183+
expect(getByText(en.eModeBanner.manageEModeButtonLabel)).toBeInTheDocument();
184+
expect(queryByText(fakeChildrenContent)).toBeNull();
185+
});
152186

153-
(TokenAnnouncement as Mock).mockImplementation(() => null);
187+
it('renders default announcement about borrow being disabled due to isolation mode settings if the asset is not borrowable by the user', async () => {
188+
mockGetPool(
189+
getCustomFakePool({
190+
pool: {
191+
...fakePool,
192+
userEModeGroup: fakePool.eModeGroups[2],
193+
},
194+
asset: {
195+
isBorrowableByUser: false,
196+
},
197+
}),
198+
);
154199

155-
const { container, queryByText } = renderComponent(
156-
<AssetAccessor {...fakeProps}>{() => <TestComponent />}</AssetAccessor>,
200+
const { container, getByText, queryByText } = renderComponent(
201+
<AssetAccessor {...borrowProps}>{() => <TestComponent />}</AssetAccessor>,
157202
);
158203

159-
expect(container.textContent).toMatchSnapshot();
204+
await waitFor(() =>
205+
expect(container).toHaveTextContent(
206+
'Your isolation mode settings do not allow you to borrow from this market.',
207+
),
208+
);
160209

210+
expect(getByText(en.eModeBanner.manageEModeButtonLabel)).toBeInTheDocument();
161211
expect(queryByText(fakeChildrenContent)).toBeNull();
162212
});
163213

@@ -166,7 +216,7 @@ describe('containers/AssetAccessor', () => {
166216
let fetchedAsset: Asset | undefined;
167217

168218
renderComponent(
169-
<AssetAccessor {...fakeProps}>
219+
<AssetAccessor {...borrowProps}>
170220
{({ asset, pool }) => {
171221
fetchedPool = pool;
172222
fetchedAsset = asset;
@@ -181,11 +231,15 @@ describe('containers/AssetAccessor', () => {
181231

182232
await waitFor(() => expect(fetchedPool).toBe(fakePool));
183233
expect(fetchedAsset).toBe(fakeAsset);
234+
expect(mockUseGetPool).toHaveBeenCalledWith({
235+
poolComptrollerAddress: fakePool.comptrollerAddress,
236+
accountAddress: fakeAddress,
237+
});
184238
});
185239

186240
it('renders children if user has connected their wallet and enabled token', async () => {
187241
const { getByText } = renderComponent(
188-
<AssetAccessor {...fakeProps}>{() => <TestComponent />}</AssetAccessor>,
242+
<AssetAccessor {...borrowProps}>{() => <TestComponent />}</AssetAccessor>,
189243
{
190244
accountAddress: fakeAddress,
191245
},

apps/evm/src/containers/AssetAccessor/index.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,9 @@ const AssetAccessor: React.FC<AssetAccessorProps> = ({
3636
return <Spinner />;
3737
}
3838

39-
if (
40-
asset.disabledTokenActions.includes(action) ||
41-
((action === 'borrow' || action === 'boost') && !asset.isBorrowable)
42-
) {
43-
return <DisabledActionNotice token={vToken.underlyingToken} action={action} />;
44-
}
39+
const isBorrowAction = action === 'borrow' || action === 'boost';
4540

46-
if ((action === 'borrow' || action === 'boost') && !asset.isBorrowableByUser) {
41+
if (isBorrowAction && !asset.isBorrowableByUser) {
4742
const components = {
4843
Link: (
4944
<EModeButton
@@ -55,7 +50,9 @@ const AssetAccessor: React.FC<AssetAccessorProps> = ({
5550
),
5651
};
5752

58-
return (
53+
return asset.isBorrowable ? (
54+
// If the asset is normally borrowable from the pool, then the user's E-mode group settings
55+
// are preventing them from being able to borrow it
5956
<NoticeWarning
6057
description={
6158
pool.userEModeGroup?.isIsolated ? (
@@ -71,9 +68,15 @@ const AssetAccessor: React.FC<AssetAccessorProps> = ({
7168
)
7269
}
7370
/>
71+
) : (
72+
<DisabledActionNotice token={vToken.underlyingToken} action={action} />
7473
);
7574
}
7675

76+
if (asset.disabledTokenActions.includes(action)) {
77+
return <DisabledActionNotice token={vToken.underlyingToken} action={action} />;
78+
}
79+
7780
return children({ asset, pool });
7881
};
7982

0 commit comments

Comments
 (0)