Skip to content

Commit 0563ca5

Browse files
authored
Feat: sDOLA-scrvUSD markets (#101)
* add sDOLA-scrvUSD and year vault markets tests * add ALE sDOLA LP helper
1 parent 236e3f9 commit 0563ca5

9 files changed

+1596
-1
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {IMarket} from "src/interfaces/IMarket.sol";
5+
import {Sweepable, SafeERC20, IERC20} from "src/util/Sweepable.sol";
6+
import {IMultiMarketTransformHelper} from "src/interfaces/IMultiMarketTransformHelper.sol";
7+
import {ICurvePool} from "src/interfaces/ICurvePool.sol";
8+
import {IYearnVaultV2} from "src/interfaces/IYearnVaultV2.sol";
9+
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
10+
/**
11+
* @title CurveLP Helper for ALE and Market for sDOLA Curve pools using dynamic array when adding liquidity
12+
* @notice This contract is a generalized ALE helper contract for a curve pools with sDOLA. Also support YearnV2 vaults for this LP.
13+
* @dev This contract is used by the ALE to interact with sDOLA Curve pools or YearnV2 Curve vaults and market.
14+
* Can also be used by anyone to perform add/remove liquidity from and to DOLA and deposit/withdraw operations.
15+
**/
16+
17+
contract CurveSDolaLPHelperDynamic is Sweepable, IMultiMarketTransformHelper {
18+
using SafeERC20 for IERC20;
19+
20+
error InsufficientLP();
21+
error InsufficientShares();
22+
error MarketNotSet(address market);
23+
24+
struct Pool {
25+
ICurvePool pool;
26+
uint128 sDolaIndex;
27+
uint128 length;
28+
IYearnVaultV2 vault;
29+
}
30+
31+
event MarketSet(
32+
address indexed market,
33+
uint128 sDolaIndex,
34+
address indexed pool,
35+
address indexed yearnVault
36+
);
37+
event MarketRemoved(address indexed market);
38+
39+
IERC20 public immutable DOLA;
40+
IERC4626 public immutable sDOLA;
41+
/// @notice Mapping of market addresses to their associated Curve Pools.
42+
mapping(address => Pool) public markets;
43+
44+
/** @dev Constructor
45+
@param _gov The address of Inverse Finance governance
46+
@param _guardian The address of the guardian
47+
@param _dola The address of DOLA
48+
@param _sDola The address of sDOLA
49+
**/
50+
constructor(
51+
address _gov,
52+
address _guardian,
53+
address _dola,
54+
address _sDola
55+
) Sweepable(_gov, _guardian) {
56+
DOLA = IERC20(_dola);
57+
sDOLA = IERC4626(_sDola);
58+
}
59+
60+
/**
61+
* @notice Deposits DOLA into the Curve Pool and returns the received LP token.
62+
* @dev Used by the ALE but can be called by anyone.
63+
* @param amount The amount of DOLA to be deposited.
64+
* @param data The encoded address of the market.
65+
* @return collateralAmount The amount of LP token received.
66+
*/
67+
function transformToCollateral(
68+
uint256 amount,
69+
bytes calldata data
70+
) external override returns (uint256 collateralAmount) {
71+
collateralAmount = transformToCollateral(amount, msg.sender, data);
72+
}
73+
74+
/**
75+
* @notice Deposits DOLA into the Curve Pool and returns the received LP token or Yearn token.
76+
* @dev Use custom recipient address.
77+
* @param amount The amount of DOLA to be deposited.
78+
* @param recipient The address on behalf of which the collateralAmount are deposited.
79+
* @param data The encoded address of the market.
80+
* @return collateralAmount The amount of LP or Yearn token received.
81+
*/
82+
function transformToCollateral(
83+
uint256 amount,
84+
address recipient,
85+
bytes calldata data
86+
) public override returns (uint256 collateralAmount) {
87+
(address market, uint256 minMint) = abi.decode(
88+
data,
89+
(address, uint256)
90+
);
91+
_revertIfMarketNotSet(market);
92+
93+
IYearnVaultV2 vault = markets[market].vault;
94+
95+
// If vault is set, add DOLA liquidity to Curve Pool and then deposit the LP token into the Yearn Vault
96+
if (address(vault) != address(0)) {
97+
uint256 lpAmount = _addLiquidity(
98+
market,
99+
amount,
100+
minMint,
101+
address(this)
102+
);
103+
IERC20(address(markets[market].pool)).approve(
104+
address(vault),
105+
lpAmount
106+
);
107+
return vault.deposit(lpAmount, recipient);
108+
} else {
109+
// Just add DOLA liquidity to the pool
110+
return _addLiquidity(market, amount, minMint, recipient);
111+
}
112+
}
113+
114+
/**
115+
* @notice Redeems the LP or Yearn token for DOLA.
116+
* @dev Used by the ALE but can be called by anyone.
117+
* @param amount The amount of LP or Yearn token to be redeemed.
118+
* @param data The encoded address of the market.
119+
* @return dolaAmount The amount of DOLA redeemed.
120+
*/
121+
function transformFromCollateral(
122+
uint256 amount,
123+
bytes calldata data
124+
) external override returns (uint256 dolaAmount) {
125+
return transformFromCollateral(amount, msg.sender, data);
126+
}
127+
128+
/**
129+
* @notice Redeems Collateral for DOLA.
130+
* @dev Use custom recipient address.
131+
* @param amount The amount of LP or Yearn Token to be redeemed.
132+
* @param recipient The address to which the underlying token is transferred.
133+
* @param data The encoded address of the market.
134+
* @return dolaAmount The amount of DOLA redeemed.
135+
*/
136+
function transformFromCollateral(
137+
uint256 amount,
138+
address recipient,
139+
bytes calldata data
140+
) public override returns (uint256 dolaAmount) {
141+
(address market, uint256 minOut) = abi.decode(data, (address, uint256));
142+
_revertIfMarketNotSet(market);
143+
144+
ICurvePool pool = markets[market].pool;
145+
IYearnVaultV2 vault = markets[market].vault;
146+
uint128 sDolaIndex = markets[market].sDolaIndex;
147+
148+
uint256 lpAmount;
149+
// If vault is set, withdraw LP token from the Yearn Vault and then remove liquidity from the pool
150+
if (address(vault) != address(0)) {
151+
IERC20(address(vault)).safeTransferFrom(
152+
msg.sender,
153+
address(this),
154+
amount
155+
);
156+
lpAmount = vault.withdraw(amount);
157+
_reimburseSharesLeft(vault, recipient);
158+
} else {
159+
// Just remove liquidity from the pool
160+
IERC20(address(pool)).safeTransferFrom(
161+
msg.sender,
162+
address(this),
163+
amount
164+
);
165+
lpAmount = amount;
166+
}
167+
return _removeLiquidity(pool, lpAmount, sDolaIndex, minOut, recipient);
168+
}
169+
170+
/**
171+
* @notice Convert DOLA into LP or Yearn token and deposit the received amount for recipient.
172+
* @param assets The amount of DOLA to be converted.
173+
* @param recipient The address on behalf of which the LP or Yearn are deposited.
174+
* @param data The encoded address of the market.
175+
* @return collateralAmount The amount of collateral deposited into the market.
176+
*/
177+
function transformToCollateralAndDeposit(
178+
uint256 assets,
179+
address recipient,
180+
bytes calldata data
181+
) external override returns (uint256) {
182+
(address market, ) = abi.decode(data, (address, uint256));
183+
_revertIfMarketNotSet(market);
184+
185+
// Convert DOLA to LP or Yearn token
186+
uint256 amount = transformToCollateral(assets, address(this), data);
187+
188+
IYearnVaultV2 vault = markets[market].vault;
189+
190+
uint256 actualAmount;
191+
address collateral;
192+
193+
// If Vault is set, deposit the Yearn token into the market
194+
if (address(vault) != address(0)) {
195+
collateral = address(vault);
196+
actualAmount = vault.balanceOf(address(this));
197+
} else {
198+
// Deposit the LP token into the market
199+
collateral = address(markets[market].pool);
200+
actualAmount = IERC20(collateral).balanceOf(address(this));
201+
}
202+
203+
if (amount > actualAmount) revert InsufficientShares();
204+
205+
IERC20(collateral).approve(market, actualAmount);
206+
IMarket(market).deposit(recipient, actualAmount);
207+
return actualAmount;
208+
}
209+
210+
/**
211+
* @notice Withdraw the collateral from the market then convert to DOLA.
212+
* @param amount The amount of LP or Yearn token to be withdrawn from the market.
213+
* @param recipient The address to which DOLA is transferred.
214+
* @param permit The permit data for the Market.
215+
* @param data The encoded address of the market.
216+
* @return dolaAmount The amount of DOLA redeemed.
217+
*/
218+
function withdrawAndTransformFromCollateral(
219+
uint256 amount,
220+
address recipient,
221+
Permit calldata permit,
222+
bytes calldata data
223+
) external override returns (uint256 dolaAmount) {
224+
(address market, uint256 minOut) = abi.decode(data, (address, uint256));
225+
_revertIfMarketNotSet(market);
226+
227+
IMarket(market).withdrawOnBehalf(
228+
msg.sender,
229+
amount,
230+
permit.deadline,
231+
permit.v,
232+
permit.r,
233+
permit.s
234+
);
235+
236+
ICurvePool pool = markets[market].pool;
237+
IYearnVaultV2 vault = markets[market].vault;
238+
239+
// Withdraw from the vault if it is set and then remove liquidity from the pool
240+
if (address(vault) != address(0)) {
241+
amount = vault.withdraw(amount);
242+
_reimburseSharesLeft(vault, recipient);
243+
}
244+
// Just remove liquidity from the pool
245+
if (IERC20(address(pool)).balanceOf(address(this)) < amount)
246+
revert InsufficientLP();
247+
return
248+
_removeLiquidity(
249+
pool,
250+
amount,
251+
markets[market].sDolaIndex,
252+
minOut,
253+
recipient
254+
);
255+
}
256+
257+
function _addLiquidity(
258+
address market,
259+
uint256 amount,
260+
uint256 minMint,
261+
address recipient
262+
) internal returns (uint256 lpAmount) {
263+
DOLA.safeTransferFrom(msg.sender, address(this), amount);
264+
265+
DOLA.approve(address(sDOLA), amount);
266+
uint256 sDolaAmount = sDOLA.deposit(amount, address(this));
267+
268+
uint128 sDolaIndex = markets[market].sDolaIndex;
269+
ICurvePool pool = markets[market].pool;
270+
sDOLA.approve(address(pool), sDolaAmount);
271+
272+
uint256[] memory amounts = new uint256[](markets[market].length);
273+
amounts[sDolaIndex] = sDolaAmount;
274+
return pool.add_liquidity(amounts, minMint, recipient);
275+
}
276+
277+
function _removeLiquidity(
278+
ICurvePool pool,
279+
uint256 amount,
280+
uint128 sDolaIndex,
281+
uint256 minOut,
282+
address recipient
283+
) internal returns (uint256 dolaAmount) {
284+
uint256 sDolaAmount = pool.remove_liquidity_one_coin(
285+
amount,
286+
int128(sDolaIndex),
287+
minOut,
288+
address(this)
289+
);
290+
return sDOLA.redeem(sDolaAmount, recipient, address(this));
291+
}
292+
293+
function _reimburseSharesLeft(
294+
IYearnVaultV2 vault,
295+
address recipient
296+
) internal {
297+
uint256 sharesLeft = vault.balanceOf(address(this));
298+
if (sharesLeft > 0)
299+
IERC20(address(vault)).safeTransfer(recipient, sharesLeft);
300+
}
301+
302+
function _revertIfMarketNotSet(address market) internal view {
303+
if (address(markets[market].pool) == address(0))
304+
revert MarketNotSet(market);
305+
}
306+
307+
/**
308+
* @notice Set the market address and its associated Curve Pool and sDola Index.
309+
* @dev Only callable by the governance.
310+
* @param marketAddress The address of the market.
311+
* @param sDolaIndex sDola index in the coins array for Curve Pools.
312+
* @param poolAddress The address of the curve pool with sDOLA.
313+
*/
314+
function setMarket(
315+
address marketAddress,
316+
address poolAddress,
317+
uint128 sDolaIndex,
318+
uint128 length,
319+
address vaultAddress
320+
) external onlyGov {
321+
markets[marketAddress] = Pool({
322+
pool: ICurvePool(poolAddress),
323+
sDolaIndex: sDolaIndex,
324+
length: length,
325+
vault: IYearnVaultV2(vaultAddress)
326+
});
327+
emit MarketSet(marketAddress, sDolaIndex, poolAddress, vaultAddress);
328+
}
329+
330+
/**
331+
* @notice Remove the market.
332+
* @dev Only callable by the governance or the guardian.
333+
* @param market The address of the market to be removed.
334+
*/
335+
function removeMarket(address market) external onlyGuardianOrGov {
336+
delete markets[market];
337+
emit MarketRemoved(market);
338+
}
339+
}

test/ConfigAddr.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ contract ConfigAddr {
1212
address borrowControllerAddr =
1313
address(0x44B7895989Bc7886423F06DeAa844D413384b0d6);
1414
address fedAddr = address(0x2b34548b865ad66A2B046cb82e59eE43F75B90fd);
15+
address sDolaAddr = address(0xb45ad160634c528Cc3D2926d9807104FA3157305);
1516

1617
// Mainnet Dola Flash Minter
1718
address flashMinterAddr =

0 commit comments

Comments
 (0)