Skip to content

Commit

Permalink
Merge pull request #205 from yieldprotocol/feat/remove-and-redeem
Browse files Browse the repository at this point in the history
Remove and repay, remove and redeem
  • Loading branch information
alcueca authored May 26, 2021
2 parents d54f9eb + 12f107b commit 998fe17
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 49 deletions.
70 changes: 54 additions & 16 deletions contracts/Ladle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ contract Ladle is LadleStorage, AccessControl() {
(cachedId, vault) = (vaultId, _build(vaultId, seriesId, ilkId)); // Cache the vault that was just built

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

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

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

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

} else if (operation == Operation.REMOVE_REPAY) {
(bytes12 vaultId, address to, uint128 minBaseOut, uint128 minFYTokenOut) = abi.decode(data[i], (bytes12, address, uint128, uint128));
} else if (operation == Operation.REPAY_LADLE) {
(bytes12 vaultId) = abi.decode(data[i], (bytes12));
if (cachedId != vaultId) (cachedId, vault) = (vaultId, getOwnedVault(vaultId));
_removeAndRepay(vaultId, vault, to, minBaseOut, minFYTokenOut);
_repayLadle(vaultId, vault);

} else if (operation == Operation.RETRIEVE) {
(bytes6 assetId, bool isAsset, address to) = abi.decode(data[i], (bytes6, bool, address));
_retrieve(assetId, isAsset, to);

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

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

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

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

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

/// @dev Remove liquidity in a pool and use proceedings to repay debt
/// The liquidity tokens need to be already in the pool, unaccounted for.
function _removeAndRepay(bytes12 vaultId, DataTypes.Vault memory vault, address to, uint128 minBaseOut, uint128 minFYTokenOut)
/// Only before maturity. TODO: After maturity
/* function _removeAndRepay(bytes12 vaultId, DataTypes.Vault memory vault, address to, uint128 minBaseOut, uint128 minFYTokenOut)
private
returns (DataTypes.Balances memory balances)
{
Expand All @@ -462,6 +471,34 @@ contract Ladle is LadleStorage, AccessControl() {
IERC20 fyToken = IERC20(address(series.fyToken));
fyToken.safeTransfer(to, art - repayment);
}
} */

// ---- Ladle as a token holder ----

/// @dev Use fyToken in the Ladle to repay debt.
function _repayLadle(bytes12 vaultId, DataTypes.Vault memory vault)
private
returns (DataTypes.Balances memory balances)
{
DataTypes.Series memory series = getSeries(vault.seriesId);
balances = cauldron.balances(vaultId);

uint256 amount = series.fyToken.balanceOf(address(this));
amount = amount <= balances.art ? amount : balances.art;

// Update accounting
balances = cauldron.pour(vaultId, 0, -(amount.u128().i128()));
series.fyToken.burn(address(this), amount);
}

/// @dev Retrieve any asset or fyToken in the Ladle
function _retrieve(bytes6 id, bool isAsset, address to)
private
returns (uint256 amount)
{
IERC20 token = IERC20(findToken(id, isAsset));
amount = token.balanceOf(address(this));
token.safeTransfer(to, amount);
}

// ---- Liquidations ----
Expand Down Expand Up @@ -489,26 +526,26 @@ contract Ladle is LadleStorage, AccessControl() {
// ---- Permit management ----

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

/// @dev Execute an ERC2612 permit for the selected asset or fyToken
function _forwardPermit(bytes6 id, bool asset, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
function _forwardPermit(bytes6 id, bool isAsset, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
private
{
IERC2612 token = IERC2612(findToken(id, asset));
IERC2612 token = IERC2612(findToken(id, isAsset));
token.permit(msg.sender, spender, amount, deadline, v, r, s);
}

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

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

/// @dev Allow users to redeem fyToken, to be used with batch
/// @dev Allow users to redeem fyToken, to be used with batch.
/// If 0 is passed as the amount to redeem, it redeems the fyToken balance of the Ladle instead.
function _redeem(IFYToken fyToken, address to, uint256 wad)
private
returns (uint256)
{
return fyToken.redeem(to, wad);
return fyToken.redeem(to, wad != 0 ? wad : fyToken.balanceOf(address(this)));
}

// ---- Module router ----
Expand Down
21 changes: 11 additions & 10 deletions contracts/LadleStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ contract LadleStorage {
CLOSE, // 8
REPAY, // 9
REPAY_VAULT, // 10
REMOVE_REPAY, // 11
FORWARD_PERMIT, // 12
FORWARD_DAI_PERMIT, // 13
JOIN_ETHER, // 14
EXIT_ETHER, // 15
TRANSFER_TO_POOL, // 16
ROUTE, // 17
TRANSFER_TO_FYTOKEN, // 18
REDEEM, // 19
MODULE // 20
REPAY_LADLE, // 11
RETRIEVE, // 12
FORWARD_PERMIT, // 13
FORWARD_DAI_PERMIT, // 14
JOIN_ETHER, // 15
EXIT_ETHER, // 16
TRANSFER_TO_POOL, // 17
ROUTE, // 18
TRANSFER_TO_FYTOKEN, // 19
REDEEM, // 20
MODULE // 21
}

ICauldron public immutable cauldron;
Expand Down
21 changes: 11 additions & 10 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ export const OPS = {
CLOSE: 8,
REPAY: 9,
REPAY_VAULT: 10,
REMOVE_REPAY: 11,
FORWARD_PERMIT: 12,
FORWARD_DAI_PERMIT: 13,
JOIN_ETHER: 14,
EXIT_ETHER: 15,
TRANSFER_TO_POOL: 16,
ROUTE: 17,
TRANSFER_TO_FYTOKEN: 18,
REDEEM: 19,
MODULE: 20,
REPAY_LADLE: 11,
RETRIEVE: 12,
FORWARD_PERMIT: 13,
FORWARD_DAI_PERMIT: 14,
JOIN_ETHER: 15,
EXIT_ETHER: 16,
TRANSFER_TO_POOL: 17,
ROUTE: 18,
TRANSFER_TO_FYTOKEN: 19,
REDEEM: 20,
MODULE: 21,
}

export const CHI = ethers.utils.formatBytes32String('chi').slice(0, 14)
Expand Down
32 changes: 20 additions & 12 deletions src/ladleWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,20 @@ export class LadleWrapper {
return this.batch([this.repayVaultAction(vaultId, to, ink, max)])
}

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

public async removeRepay(vaultId: string, to: string, minBaseOut: BigNumberish, minFYTokenOut: BigNumberish): Promise<ContractTransaction> {
return this.batch([this.removeRepayAction(vaultId, to, minBaseOut, minFYTokenOut)])
public async repayLadle(vaultId: string): Promise<ContractTransaction> {
return this.batch([this.repayLadleAction(vaultId)])
}

public retrieveAction(assetId: string, isAsset: boolean, to: string): BatchAction {
return new BatchAction(OPS.RETRIEVE, ethers.utils.defaultAbiCoder.encode(['bytes6', 'bool', 'address'], [assetId, isAsset, to]))
}

public async retrieve(assetId: string, isAsset: boolean, to: string): Promise<ContractTransaction> {
return this.batch([this.retrieveAction(assetId, isAsset, to)])
}

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

public forwardPermitAction(id: string, asset: boolean, spender: string, amount: BigNumberish, deadline: BigNumberish, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
public forwardPermitAction(id: string, isAsset: boolean, spender: string, amount: BigNumberish, deadline: BigNumberish, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
return new BatchAction(OPS.FORWARD_PERMIT, ethers.utils.defaultAbiCoder.encode(
['bytes6', 'bool', 'address', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'],
[id, asset, spender, amount, deadline, v, r, s]
[id, isAsset, spender, amount, deadline, v, r, s]
))
}

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

public forwardDaiPermitAction(id: string, asset: boolean, spender: string, nonce: BigNumberish, deadline: BigNumberish, approved: boolean, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
public forwardDaiPermitAction(id: string, isAsset: boolean, spender: string, nonce: BigNumberish, deadline: BigNumberish, approved: boolean, v: BigNumberish, r: Buffer, s: Buffer): BatchAction {
return new BatchAction(OPS.FORWARD_DAI_PERMIT, ethers.utils.defaultAbiCoder.encode(
['bytes6', 'bool', 'address', 'uint256', 'uint256', 'bool', 'uint8', 'bytes32', 'bytes32'],
[id, asset, spender, nonce, deadline, approved, v, r, s]
[id, isAsset, spender, nonce, deadline, approved, v, r, s]
))
}

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

public joinEtherAction(etherId: string): BatchAction {
Expand Down
49 changes: 48 additions & 1 deletion test/066_remove_and_repay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { WAD, MAX128 } = constants
const MAX = MAX128

import { Cauldron } from '../typechain/Cauldron'
import { Join } from '../typechain/Join'
import { FYToken } from '../typechain/FYToken'
import { PoolMock } from '../typechain/PoolMock'
import { ERC20Mock } from '../typechain/ERC20Mock'
Expand All @@ -28,6 +29,7 @@ describe('Ladle - remove and repay', function () {
let fyToken: FYToken
let pool: PoolMock
let base: ERC20Mock
let baseJoin: Join
let ilk: ERC20Mock
let ladle: LadleWrapper

Expand Down Expand Up @@ -55,6 +57,7 @@ describe('Ladle - remove and repay', function () {
cauldron = env.cauldron
ladle = env.ladle
base = env.assets.get(baseId) as ERC20Mock
baseJoin = env.joins.get(baseId) as Join
ilk = env.assets.get(ilkId) as ERC20Mock
fyToken = env.series.get(seriesId) as FYToken
pool = env.pools.get(seriesId) as PoolMock
Expand All @@ -65,6 +68,10 @@ describe('Ladle - remove and repay', function () {
await ladle.serve(vaultId, pool.address, WAD, WAD, MAX)
await ladle.pour(vaultId, pool.address, WAD.mul(2), WAD.mul(2))
await pool.mint(owner, true, 0)

// Add some base to the baseJoin to serve redemptions
await base.mint(baseJoin.address, WAD.mul(3))
await baseJoin.join(owner, WAD.mul(3))
})

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

await pool.transfer(pool.address, WAD.mul(2))
await ladle.removeRepay(vaultId, owner, 0, 0)

const burnCall = pool.interface.encodeFunctionData('burn', [ladle.address, 0, 0])

await ladle.batch([
ladle.routeAction(seriesId, burnCall), // burn to ladle
ladle.repayLadleAction(vaultId), // ladle repay
ladle.retrieveAction(seriesId, false, owner), // retrieve fyToken
ladle.retrieveAction(baseId, true, owner), // retrieve base
])

const baseOut = baseReservesBefore.sub(await base.balanceOf(pool.address))
const fyTokenOut = fyTokenReservesBefore.sub(await fyToken.balanceOf(pool.address))
Expand All @@ -85,4 +100,36 @@ describe('Ladle - remove and repay', function () {
expect(fyTokenOut).to.equal(debtRepaid.add(fyTokenObtained))
expect(baseObtained).to.equal(baseOut)
})

describe('after maturity', async () => {
beforeEach(async () => {
await ethers.provider.send('evm_mine', [(await fyToken.maturity()).toNumber()])
})

it('redeems fyToken, returns base', async () => {
const baseReservesBefore = await base.balanceOf(pool.address)
const joinReservesBefore = await base.balanceOf(baseJoin.address)
const fyTokenReservesBefore = await fyToken.balanceOf(pool.address)
const fyTokenSupplyBefore = await fyToken.totalSupply()
const baseBalanceBefore = await base.balanceOf(owner)

await pool.transfer(pool.address, WAD.mul(2))
const burnCall = pool.interface.encodeFunctionData('burn', [ladle.address, 0, 0])

await ladle.batch([
ladle.routeAction(seriesId, burnCall), // burn to ladle
ladle.redeemAction(seriesId, owner, 0), // ladle redeem
ladle.retrieveAction(baseId, true, owner), // retrieve base
])

const baseOut = baseReservesBefore.sub(await base.balanceOf(pool.address))
const fyTokenSupply = await fyToken.totalSupply()
const fyTokenRedeemed = fyTokenSupplyBefore.sub(await fyToken.totalSupply())
const baseServed = joinReservesBefore.sub(await base.balanceOf(baseJoin.address))

const baseObtained = (await base.balanceOf(owner)).sub(baseBalanceBefore)
expect(baseObtained).to.equal(baseOut.add(baseServed))
expect(fyTokenSupply).to.equal(fyTokenSupplyBefore.sub(fyTokenRedeemed))
})
})
})

0 comments on commit 998fe17

Please sign in to comment.