Skip to content

Commit 998fe17

Browse files
authored
Merge pull request #205 from yieldprotocol/feat/remove-and-redeem
Remove and repay, remove and redeem
2 parents d54f9eb + 12f107b commit 998fe17

File tree

5 files changed

+144
-49
lines changed

5 files changed

+144
-49
lines changed

contracts/Ladle.sol

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ contract Ladle is LadleStorage, AccessControl() {
136136
(cachedId, vault) = (vaultId, _build(vaultId, seriesId, ilkId)); // Cache the vault that was just built
137137

138138
} else if (operation == Operation.FORWARD_PERMIT) {
139-
(bytes6 id, bool asset, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) =
139+
(bytes6 id, bool isAsset, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) =
140140
abi.decode(data[i], (bytes6, bool, address, uint256, uint256, uint8, bytes32, bytes32));
141-
_forwardPermit(id, asset, spender, amount, deadline, v, r, s);
141+
_forwardPermit(id, isAsset, spender, amount, deadline, v, r, s);
142142

143143
} else if (operation == Operation.JOIN_ETHER) {
144144
(bytes6 etherId) = abi.decode(data[i], (bytes6));
@@ -160,9 +160,9 @@ contract Ladle is LadleStorage, AccessControl() {
160160
(vault,) = _roll(vaultId, vault, newSeriesId, loan, max);
161161

162162
} else if (operation == Operation.FORWARD_DAI_PERMIT) {
163-
(bytes6 id, bool asset, address spender, uint256 nonce, uint256 deadline, bool allowed, uint8 v, bytes32 r, bytes32 s) =
163+
(bytes6 id, bool isAsset, address spender, uint256 nonce, uint256 deadline, bool allowed, uint8 v, bytes32 r, bytes32 s) =
164164
abi.decode(data[i], (bytes6, bool, address, uint256, uint256, bool, uint8, bytes32, bytes32));
165-
_forwardDaiPermit(id, asset, spender, nonce, deadline, allowed, v, r, s);
165+
_forwardDaiPermit(id, isAsset, spender, nonce, deadline, allowed, v, r, s);
166166

167167
} else if (operation == Operation.TRANSFER_TO_POOL) {
168168
(bytes6 seriesId, bool base, uint128 wad) =
@@ -195,10 +195,14 @@ contract Ladle is LadleStorage, AccessControl() {
195195
if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId));
196196
_repayVault(vaultId, vault, to, ink, max);
197197

198-
} else if (operation == Operation.REMOVE_REPAY) {
199-
(bytes12 vaultId, address to, uint128 minBaseOut, uint128 minFYTokenOut) = abi.decode(data[i], (bytes12, address, uint128, uint128));
198+
} else if (operation == Operation.REPAY_LADLE) {
199+
(bytes12 vaultId) = abi.decode(data[i], (bytes12));
200200
if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId));
201-
_removeAndRepay(vaultId, vault, to, minBaseOut, minFYTokenOut);
201+
_repayLadle(vaultId, vault);
202+
203+
} else if (operation == Operation.RETRIEVE) {
204+
(bytes6 assetId, bool isAsset, address to) = abi.decode(data[i], (bytes6, bool, address));
205+
_retrieve(assetId, isAsset, to);
202206

203207
} else if (operation == Operation.TRANSFER_TO_FYTOKEN) {
204208
(bytes6 seriesId, uint256 amount) = abi.decode(data[i], (bytes6, uint256));
@@ -289,6 +293,7 @@ contract Ladle is LadleStorage, AccessControl() {
289293

290294
/// @dev Add collateral and borrow from vault, pull assets from and push borrowed asset to user
291295
/// Or, repay to vault and remove collateral, pull borrowed asset from and push assets to user
296+
/// Borrow only before maturity.
292297
function _pour(bytes12 vaultId, DataTypes.Vault memory vault, address to, int128 ink, int128 art)
293298
private
294299
returns (DataTypes.Balances memory balances)
@@ -318,6 +323,7 @@ contract Ladle is LadleStorage, AccessControl() {
318323

319324
/// @dev Add collateral and borrow from vault, so that a precise amount of base is obtained by the user.
320325
/// The base is obtained by borrowing fyToken and buying base with it in a pool.
326+
/// Only before maturity.
321327
function _serve(bytes12 vaultId, DataTypes.Vault memory vault, address to, uint128 ink, uint128 base, uint128 max)
322328
private
323329
returns (DataTypes.Balances memory balances, uint128 art)
@@ -373,6 +379,7 @@ contract Ladle is LadleStorage, AccessControl() {
373379

374380
/// @dev Repay debt by selling base in a pool and using the resulting fyToken
375381
/// The base tokens need to be already in the pool, unaccounted for.
382+
/// Only before maturity. After maturity use close.
376383
function _repay(bytes12 vaultId, DataTypes.Vault memory vault, address to, int128 ink, uint128 min)
377384
private
378385
returns (DataTypes.Balances memory balances, uint128 art)
@@ -386,6 +393,7 @@ contract Ladle is LadleStorage, AccessControl() {
386393

387394
/// @dev Repay all debt in a vault by buying fyToken from a pool with base.
388395
/// The base tokens need to be already in the pool, unaccounted for. The surplus base will be returned to msg.sender.
396+
/// Only before maturity. After maturity use close.
389397
function _repayVault(bytes12 vaultId, DataTypes.Vault memory vault, address to, int128 ink, uint128 max)
390398
private
391399
returns (DataTypes.Balances memory balances, uint128 base)
@@ -435,7 +443,8 @@ contract Ladle is LadleStorage, AccessControl() {
435443

436444
/// @dev Remove liquidity in a pool and use proceedings to repay debt
437445
/// The liquidity tokens need to be already in the pool, unaccounted for.
438-
function _removeAndRepay(bytes12 vaultId, DataTypes.Vault memory vault, address to, uint128 minBaseOut, uint128 minFYTokenOut)
446+
/// Only before maturity. TODO: After maturity
447+
/* function _removeAndRepay(bytes12 vaultId, DataTypes.Vault memory vault, address to, uint128 minBaseOut, uint128 minFYTokenOut)
439448
private
440449
returns (DataTypes.Balances memory balances)
441450
{
@@ -462,6 +471,34 @@ contract Ladle is LadleStorage, AccessControl() {
462471
IERC20 fyToken = IERC20(address(series.fyToken));
463472
fyToken.safeTransfer(to, art - repayment);
464473
}
474+
} */
475+
476+
// ---- Ladle as a token holder ----
477+
478+
/// @dev Use fyToken in the Ladle to repay debt.
479+
function _repayLadle(bytes12 vaultId, DataTypes.Vault memory vault)
480+
private
481+
returns (DataTypes.Balances memory balances)
482+
{
483+
DataTypes.Series memory series = getSeries(vault.seriesId);
484+
balances = cauldron.balances(vaultId);
485+
486+
uint256 amount = series.fyToken.balanceOf(address(this));
487+
amount = amount <= balances.art ? amount : balances.art;
488+
489+
// Update accounting
490+
balances = cauldron.pour(vaultId, 0, -(amount.u128().i128()));
491+
series.fyToken.burn(address(this), amount);
492+
}
493+
494+
/// @dev Retrieve any asset or fyToken in the Ladle
495+
function _retrieve(bytes6 id, bool isAsset, address to)
496+
private
497+
returns (uint256 amount)
498+
{
499+
IERC20 token = IERC20(findToken(id, isAsset));
500+
amount = token.balanceOf(address(this));
501+
token.safeTransfer(to, amount);
465502
}
466503

467504
// ---- Liquidations ----
@@ -489,26 +526,26 @@ contract Ladle is LadleStorage, AccessControl() {
489526
// ---- Permit management ----
490527

491528
/// @dev From an id, which can be an assetId or a seriesId, find the resulting asset or fyToken
492-
function findToken(bytes6 id, bool asset)
529+
function findToken(bytes6 id, bool isAsset)
493530
private view returns (address token)
494531
{
495-
token = asset ? cauldron.assets(id) : address(getSeries(id).fyToken);
532+
token = isAsset ? cauldron.assets(id) : address(getSeries(id).fyToken);
496533
require (token != address(0), "Token not found");
497534
}
498535

499536
/// @dev Execute an ERC2612 permit for the selected asset or fyToken
500-
function _forwardPermit(bytes6 id, bool asset, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
537+
function _forwardPermit(bytes6 id, bool isAsset, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
501538
private
502539
{
503-
IERC2612 token = IERC2612(findToken(id, asset));
540+
IERC2612 token = IERC2612(findToken(id, isAsset));
504541
token.permit(msg.sender, spender, amount, deadline, v, r, s);
505542
}
506543

507544
/// @dev Execute a Dai-style permit for the selected asset or fyToken
508-
function _forwardDaiPermit(bytes6 id, bool asset, address spender, uint256 nonce, uint256 deadline, bool allowed, uint8 v, bytes32 r, bytes32 s)
545+
function _forwardDaiPermit(bytes6 id, bool isAsset, address spender, uint256 nonce, uint256 deadline, bool allowed, uint8 v, bytes32 r, bytes32 s)
509546
private
510547
{
511-
DaiAbstract token = DaiAbstract(findToken(id, asset));
548+
DaiAbstract token = DaiAbstract(findToken(id, isAsset));
512549
token.permit(msg.sender, spender, nonce, deadline, allowed, v, r, s);
513550
}
514551

@@ -569,12 +606,13 @@ contract Ladle is LadleStorage, AccessControl() {
569606
IERC20(fyToken).safeTransferFrom(msg.sender, address(fyToken), wad);
570607
}
571608

572-
/// @dev Allow users to redeem fyToken, to be used with batch
609+
/// @dev Allow users to redeem fyToken, to be used with batch.
610+
/// If 0 is passed as the amount to redeem, it redeems the fyToken balance of the Ladle instead.
573611
function _redeem(IFYToken fyToken, address to, uint256 wad)
574612
private
575613
returns (uint256)
576614
{
577-
return fyToken.redeem(to, wad);
615+
return fyToken.redeem(to, wad != 0 ? wad : fyToken.balanceOf(address(this)));
578616
}
579617

580618
// ---- Module router ----

contracts/LadleStorage.sol

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@ contract LadleStorage {
2020
CLOSE, // 8
2121
REPAY, // 9
2222
REPAY_VAULT, // 10
23-
REMOVE_REPAY, // 11
24-
FORWARD_PERMIT, // 12
25-
FORWARD_DAI_PERMIT, // 13
26-
JOIN_ETHER, // 14
27-
EXIT_ETHER, // 15
28-
TRANSFER_TO_POOL, // 16
29-
ROUTE, // 17
30-
TRANSFER_TO_FYTOKEN, // 18
31-
REDEEM, // 19
32-
MODULE // 20
23+
REPAY_LADLE, // 11
24+
RETRIEVE, // 12
25+
FORWARD_PERMIT, // 13
26+
FORWARD_DAI_PERMIT, // 14
27+
JOIN_ETHER, // 15
28+
EXIT_ETHER, // 16
29+
TRANSFER_TO_POOL, // 17
30+
ROUTE, // 18
31+
TRANSFER_TO_FYTOKEN, // 19
32+
REDEEM, // 20
33+
MODULE // 21
3334
}
3435

3536
ICauldron public immutable cauldron;

src/constants.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ export const OPS = {
1212
CLOSE: 8,
1313
REPAY: 9,
1414
REPAY_VAULT: 10,
15-
REMOVE_REPAY: 11,
16-
FORWARD_PERMIT: 12,
17-
FORWARD_DAI_PERMIT: 13,
18-
JOIN_ETHER: 14,
19-
EXIT_ETHER: 15,
20-
TRANSFER_TO_POOL: 16,
21-
ROUTE: 17,
22-
TRANSFER_TO_FYTOKEN: 18,
23-
REDEEM: 19,
24-
MODULE: 20,
15+
REPAY_LADLE: 11,
16+
RETRIEVE: 12,
17+
FORWARD_PERMIT: 13,
18+
FORWARD_DAI_PERMIT: 14,
19+
JOIN_ETHER: 15,
20+
EXIT_ETHER: 16,
21+
TRANSFER_TO_POOL: 17,
22+
ROUTE: 18,
23+
TRANSFER_TO_FYTOKEN: 19,
24+
REDEEM: 20,
25+
MODULE: 21,
2526
}
2627

2728
export const CHI = ethers.utils.formatBytes32String('chi').slice(0, 14)

src/ladleWrapper.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,20 @@ export class LadleWrapper {
167167
return this.batch([this.repayVaultAction(vaultId, to, ink, max)])
168168
}
169169

170-
public removeRepayAction(vaultId: string, to: string, minBaseOut: BigNumberish, minFYTokenOut: BigNumberish): BatchAction {
171-
return new BatchAction(OPS.REMOVE_REPAY, ethers.utils.defaultAbiCoder.encode(['bytes12', 'address', 'uint128', 'uint128'], [vaultId, to, minBaseOut, minFYTokenOut]))
170+
public repayLadleAction(vaultId: string): BatchAction {
171+
return new BatchAction(OPS.REPAY_LADLE, ethers.utils.defaultAbiCoder.encode(['bytes12'], [vaultId]))
172172
}
173173

174-
public async removeRepay(vaultId: string, to: string, minBaseOut: BigNumberish, minFYTokenOut: BigNumberish): Promise<ContractTransaction> {
175-
return this.batch([this.removeRepayAction(vaultId, to, minBaseOut, minFYTokenOut)])
174+
public async repayLadle(vaultId: string): Promise<ContractTransaction> {
175+
return this.batch([this.repayLadleAction(vaultId)])
176+
}
177+
178+
public retrieveAction(assetId: string, isAsset: boolean, to: string): BatchAction {
179+
return new BatchAction(OPS.RETRIEVE, ethers.utils.defaultAbiCoder.encode(['bytes6', 'bool', 'address'], [assetId, isAsset, to]))
180+
}
181+
182+
public async retrieve(assetId: string, isAsset: boolean, to: string): Promise<ContractTransaction> {
183+
return this.batch([this.retrieveAction(assetId, isAsset, to)])
176184
}
177185

178186
public rollAction(vaultId: string, newSeriesId: string, loan: BigNumberish, max: BigNumberish): BatchAction {
@@ -183,26 +191,26 @@ export class LadleWrapper {
183191
return this.batch([this.rollAction(vaultId, newSeriesId, loan, max)])
184192
}
185193

186-
public forwardPermitAction(id: string, asset: boolean, spender: string, amount: BigNumberish, deadline: BigNumberish, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
194+
public forwardPermitAction(id: string, isAsset: boolean, spender: string, amount: BigNumberish, deadline: BigNumberish, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
187195
return new BatchAction(OPS.FORWARD_PERMIT, ethers.utils.defaultAbiCoder.encode(
188196
['bytes6', 'bool', 'address', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'],
189-
[id, asset, spender, amount, deadline, v, r, s]
197+
[id, isAsset, spender, amount, deadline, v, r, s]
190198
))
191199
}
192200

193-
public async forwardPermit(id: string, asset: boolean, spender: string, amount: BigNumberish, deadline: BigNumberish, v: BigNumberish, r: Buffer, s: Buffer): Promise<ContractTransaction> {
194-
return this.batch([this.forwardPermitAction(id, asset, spender, amount, deadline, v, r, s)])
201+
public async forwardPermit(id: string, isAsset: boolean, spender: string, amount: BigNumberish, deadline: BigNumberish, v: BigNumberish, r: Buffer, s: Buffer): Promise<ContractTransaction> {
202+
return this.batch([this.forwardPermitAction(id, isAsset, spender, amount, deadline, v, r, s)])
195203
}
196204

197-
public forwardDaiPermitAction(id: string, asset: boolean, spender: string, nonce: BigNumberish, deadline: BigNumberish, approved: boolean, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
205+
public forwardDaiPermitAction(id: string, isAsset: boolean, spender: string, nonce: BigNumberish, deadline: BigNumberish, approved: boolean, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
198206
return new BatchAction(OPS.FORWARD_DAI_PERMIT, ethers.utils.defaultAbiCoder.encode(
199207
['bytes6', 'bool', 'address', 'uint256', 'uint256', 'bool', 'uint8', 'bytes32', 'bytes32'],
200-
[id, asset, spender, nonce, deadline, approved, v, r, s]
208+
[id, isAsset, spender, nonce, deadline, approved, v, r, s]
201209
))
202210
}
203211

204-
public async forwardDaiPermit(id: string, asset: boolean, spender: string, nonce: BigNumberish, deadline: BigNumberish, approved: boolean, v: BigNumberish, r: Buffer, s: Buffer): Promise<ContractTransaction> {
205-
return this.batch([this.forwardDaiPermitAction(id, asset, spender, nonce, deadline, approved, v, r, s)])
212+
public async forwardDaiPermit(id: string, isAsset: boolean, spender: string, nonce: BigNumberish, deadline: BigNumberish, approved: boolean, v: BigNumberish, r: Buffer, s: Buffer): Promise<ContractTransaction> {
213+
return this.batch([this.forwardDaiPermitAction(id, isAsset, spender, nonce, deadline, approved, v, r, s)])
206214
}
207215

208216
public joinEtherAction(etherId: string): BatchAction {

test/066_remove_and_repay.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { WAD, MAX128 } = constants
55
const MAX = MAX128
66

77
import { Cauldron } from '../typechain/Cauldron'
8+
import { Join } from '../typechain/Join'
89
import { FYToken } from '../typechain/FYToken'
910
import { PoolMock } from '../typechain/PoolMock'
1011
import { ERC20Mock } from '../typechain/ERC20Mock'
@@ -28,6 +29,7 @@ describe('Ladle - remove and repay', function () {
2829
let fyToken: FYToken
2930
let pool: PoolMock
3031
let base: ERC20Mock
32+
let baseJoin: Join
3133
let ilk: ERC20Mock
3234
let ladle: LadleWrapper
3335

@@ -55,6 +57,7 @@ describe('Ladle - remove and repay', function () {
5557
cauldron = env.cauldron
5658
ladle = env.ladle
5759
base = env.assets.get(baseId) as ERC20Mock
60+
baseJoin = env.joins.get(baseId) as Join
5861
ilk = env.assets.get(ilkId) as ERC20Mock
5962
fyToken = env.series.get(seriesId) as FYToken
6063
pool = env.pools.get(seriesId) as PoolMock
@@ -65,6 +68,10 @@ describe('Ladle - remove and repay', function () {
6568
await ladle.serve(vaultId, pool.address, WAD, WAD, MAX)
6669
await ladle.pour(vaultId, pool.address, WAD.mul(2), WAD.mul(2))
6770
await pool.mint(owner, true, 0)
71+
72+
// Add some base to the baseJoin to serve redemptions
73+
await base.mint(baseJoin.address, WAD.mul(3))
74+
await baseJoin.join(owner, WAD.mul(3))
6875
})
6976

7077
it('repays debt with fyToken, returns base and surplus fyToken', async () => {
@@ -75,7 +82,15 @@ describe('Ladle - remove and repay', function () {
7582
const debtBefore = (await cauldron.balances(vaultId)).art
7683

7784
await pool.transfer(pool.address, WAD.mul(2))
78-
await ladle.removeRepay(vaultId, owner, 0, 0)
85+
86+
const burnCall = pool.interface.encodeFunctionData('burn', [ladle.address, 0, 0])
87+
88+
await ladle.batch([
89+
ladle.routeAction(seriesId, burnCall), // burn to ladle
90+
ladle.repayLadleAction(vaultId), // ladle repay
91+
ladle.retrieveAction(seriesId, false, owner), // retrieve fyToken
92+
ladle.retrieveAction(baseId, true, owner), // retrieve base
93+
])
7994

8095
const baseOut = baseReservesBefore.sub(await base.balanceOf(pool.address))
8196
const fyTokenOut = fyTokenReservesBefore.sub(await fyToken.balanceOf(pool.address))
@@ -85,4 +100,36 @@ describe('Ladle - remove and repay', function () {
85100
expect(fyTokenOut).to.equal(debtRepaid.add(fyTokenObtained))
86101
expect(baseObtained).to.equal(baseOut)
87102
})
103+
104+
describe('after maturity', async () => {
105+
beforeEach(async () => {
106+
await ethers.provider.send('evm_mine', [(await fyToken.maturity()).toNumber()])
107+
})
108+
109+
it('redeems fyToken, returns base', async () => {
110+
const baseReservesBefore = await base.balanceOf(pool.address)
111+
const joinReservesBefore = await base.balanceOf(baseJoin.address)
112+
const fyTokenReservesBefore = await fyToken.balanceOf(pool.address)
113+
const fyTokenSupplyBefore = await fyToken.totalSupply()
114+
const baseBalanceBefore = await base.balanceOf(owner)
115+
116+
await pool.transfer(pool.address, WAD.mul(2))
117+
const burnCall = pool.interface.encodeFunctionData('burn', [ladle.address, 0, 0])
118+
119+
await ladle.batch([
120+
ladle.routeAction(seriesId, burnCall), // burn to ladle
121+
ladle.redeemAction(seriesId, owner, 0), // ladle redeem
122+
ladle.retrieveAction(baseId, true, owner), // retrieve base
123+
])
124+
125+
const baseOut = baseReservesBefore.sub(await base.balanceOf(pool.address))
126+
const fyTokenSupply = await fyToken.totalSupply()
127+
const fyTokenRedeemed = fyTokenSupplyBefore.sub(await fyToken.totalSupply())
128+
const baseServed = joinReservesBefore.sub(await base.balanceOf(baseJoin.address))
129+
130+
const baseObtained = (await base.balanceOf(owner)).sub(baseBalanceBefore)
131+
expect(baseObtained).to.equal(baseOut.add(baseServed))
132+
expect(fyTokenSupply).to.equal(fyTokenSupplyBefore.sub(fyTokenRedeemed))
133+
})
134+
})
88135
})

0 commit comments

Comments
 (0)