Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eth to DAI LBP #880

Merged
merged 16 commits into from
Jun 7, 2022
9 changes: 9 additions & 0 deletions contracts/pcv/balancer/BalancerLBPSwapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "./manager/WeightedBalancerPoolManager.sol";
import "./IVault.sol";
import "../../utils/Timed.sol";
import "../../refs/OracleRef.sol";
import "../../core/TribeRoles.sol";
import "../IPCVSwapper.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

Expand Down Expand Up @@ -199,6 +200,14 @@ contract BalancerLBPSwapper is IPCVSwapper, OracleRef, Timed, WeightedBalancerPo
_swap();
}

/// @notice exit LBP with all assets to this contract. The tokens can then be withdrawn via standard PCV deposit methods.
function exitPoolToSelf()
external
hasAnyOfThreeRoles(TribeRoles.GUARDIAN, TribeRoles.PCV_CONTROLLER, TribeRoles.SWAP_ADMIN_ROLE)
{
_exitPool();
}

/// @notice redeeem all assets from LP pool
/// @param to destination for withdrawn tokens
function exitPool(address to) external onlyPCVController {
Expand Down
285 changes: 285 additions & 0 deletions proposals/dao/eth_lbp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import { ethers } from 'hardhat';
import { expect } from 'chai';
import {
DeployUpgradeFunc,
NamedAddresses,
NamedContracts,
SetupUpgradeFunc,
TeardownUpgradeFunc,
ValidateUpgradeFunc
} from '@custom-types/types';
import { forceEth } from '@test/integration/setup/utils';
import { TransactionResponse } from '@ethersproject/providers';
import { expectApprox, getImpersonatedSigner, overwriteChainlinkAggregator, time } from '@test/helpers';
import { BigNumber } from 'ethers';

const toBN = ethers.BigNumber.from;

/*

DAO Proposal #110

1. Set ethToDaiLBPSwapper to be guardian Safe addresses
2. Deploy Balancer LBP and initialise auction of ETH for DAI
3. forceSwap()
4. tighten ETH redemption spread
*/

// LBP Swapper config
const LBP_FREQUENCY = 86400 * 2; // 2 days in seconds
const MIN_LBP_SIZE = ethers.constants.WeiPerEther.mul(500); // 500 ETH
let poolId; // auction pool id

const fipNumber = '110';

const deploy: DeployUpgradeFunc = async (deployAddress: string, addresses: NamedAddresses, logging: boolean) => {
/////////// 1. Deploy the Balancer LBP swapper
const BalancerLBPSwapperFactory = await ethers.getContractFactory('BalancerLBPSwapper');

// Oracle reports WETH price in terms of USD, so should not be inverted
const ethToDaiLBPSwapper = await BalancerLBPSwapperFactory.deploy(
addresses.core,
{
_oracle: addresses.chainlinkEthUsdOracleWrapper,
_backupOracle: ethers.constants.AddressZero,
_invertOraclePrice: false,
_decimalsNormalizer: 0
},
LBP_FREQUENCY,
'50000000000000000', // small weight 5%
'950000000000000000', // large weight 95%
addresses.weth,
addresses.dai,
addresses.compoundDaiPCVDeposit, // send DAI to Compound DAI deposit, where it can then be dripped to PSM
MIN_LBP_SIZE // minimum size of a pool which the swapper is used against
);

await ethToDaiLBPSwapper.deployed();
logging && console.log('WETH to DAI swapper deployed to: ', ethToDaiLBPSwapper.address);

// 2. Create a liquidity bootstrapping pool between WETH and DAI
const lbpFactory = await ethers.getContractAt(
'ILiquidityBootstrappingPoolFactory',
addresses.balancerLBPoolFactoryNoFee
);

const tx: TransactionResponse = await lbpFactory.create(
'WETH->DAI Auction Pool', // pool name
'apWETH-DAI', // lbp token symbol
[addresses.dai, addresses.weth], // pool contains [DAI, WETH]
[ethers.constants.WeiPerEther.mul(5).div(100), ethers.constants.WeiPerEther.mul(95).div(100)], // initial weights 5%/95%
ethers.constants.WeiPerEther.mul(30).div(10_000), // 0.3% swap fees
ethToDaiLBPSwapper.address, // pool owner = fei protocol swapper
true
);

const txReceipt = await tx.wait();
const { logs: rawLogs } = txReceipt;
const noFeeEthDaiLBPAddress = `0x${rawLogs[rawLogs.length - 1].topics[1].slice(-40)}`;
poolId = rawLogs[1].topics[1];

logging && console.log('LBP Pool deployed to: ', noFeeEthDaiLBPAddress);
logging && console.log('LBP Pool Id: ', poolId);

// 3. Initialise the LBP swapper with the pool address
const tx2 = await ethToDaiLBPSwapper.init(noFeeEthDaiLBPAddress);
await tx2.wait();

// 4. Deploy a lens to report the swapper value
const BPTLensFactory = await ethers.getContractFactory('BPTLens');
const ethToDaiLensDai = await BPTLensFactory.deploy(
addresses.dai, // token reported in
noFeeEthDaiLBPAddress, // pool address
addresses.chainlinkDaiUsdOracleWrapper, // reportedOracle - DAI
addresses.chainlinkEthUsdOracleWrapper, // otherOracle - WETH
false, // feiIsReportedIn
false // feiIsOther
);
await ethToDaiLensDai.deployTransaction.wait();

logging && console.log('BPTLens for DAI in swapper pool: ', ethToDaiLensDai.address);

const ethToDaiLensEth = await BPTLensFactory.deploy(
addresses.weth, // token reported in
noFeeEthDaiLBPAddress, // pool address
addresses.chainlinkEthUsdOracleWrapper, // reportedOracle - WETH
addresses.chainlinkDaiUsdOracleWrapper, // otherOracle - DAI
false, // feiIsReportedIn
false // feiIsOther
);
await ethToDaiLensEth.deployTransaction.wait();

logging && console.log('BPTLens for WETH in swapper pool: ', ethToDaiLensEth.address);
return {
ethToDaiLBPSwapper,
ethToDaiLensDai,
ethToDaiLensEth
};
};

// Do any setup necessary for running the test.
// This could include setting up Hardhat to impersonate accounts,
// ensuring contracts have a specific state, etc.
const setup: SetupUpgradeFunc = async (addresses, oldContracts, contracts, logging) => {
// check PSM redeem fee
const psm = contracts.ethPSM;
expect(await psm.redeemFeeBasisPoints()).to.be.equal('75');

// overwrite chainlink ETH/USD oracle
const ethToDaiLBPSwapper = contracts.ethToDaiLBPSwapper;
poolId = await ethToDaiLBPSwapper.pid();

await overwriteChainlinkAggregator(addresses.chainlinkEthUsdOracle, '200000000000', '8'); // $2000 ETH

// invariant checks
expect(await ethToDaiLBPSwapper.tokenSpent()).to.be.equal(addresses.weth);
expect(await ethToDaiLBPSwapper.tokenReceived()).to.be.equal(addresses.dai);
expect(await ethToDaiLBPSwapper.tokenReceivingAddress()).to.be.equal(addresses.compoundDaiPCVDeposit);

const poolTokens = await contracts.balancerVault.getPoolTokens(poolId);
expect(poolTokens.tokens[0]).to.be.equal(addresses.dai);
expect(poolTokens.tokens[1]).to.be.equal(addresses.weth);

// LBP swapper should be empty
expect(poolTokens.balances[0]).to.be.equal('0');
expect(poolTokens.balances[1]).to.be.equal('0');

// Lenses should report 0 because LBP is empty
expect(await contracts.ethToDaiLensDai.balance()).to.be.equal('0');
expect(await contracts.ethToDaiLensEth.balance()).to.be.equal('0');

// Swapper should hold no tokens
expect(await contracts.weth.balanceOf(ethToDaiLBPSwapper.address)).to.be.equal('0');
expect(await contracts.dai.balanceOf(ethToDaiLBPSwapper.address)).to.be.equal('0');

console.log('Starting DAI PSM dai balance [M]', (await contracts.compoundDaiPCVDeposit.balance()) / 1e24);

console.log('DAI needed', await ethToDaiLBPSwapper.getTokensIn(ethers.constants.WeiPerEther.mul(20_000)));
await forceEth(addresses.tribalCouncilTimelock);

await time.increase(LBP_FREQUENCY);
};

// Tears down any changes made in setup() that need to be
// cleaned up before doing any validation checks.
const teardown: TeardownUpgradeFunc = async (addresses, oldContracts, contracts, logging) => {
console.log(`No actions to complete in teardown for fip${fipNumber}`);
};

// Run any validations required on the fip using mocha or console logging
// IE check balances, check state of contracts, etc.
const validate: ValidateUpgradeFunc = async (addresses, oldContracts, contracts, logging) => {
const core = contracts.core;
const ethToDaiLBPSwapper = contracts.ethToDaiLBPSwapper;
poolId = await ethToDaiLBPSwapper.pid();

console.log('Final DAI PSM dai balance [M]', (await contracts.compoundDaiPCVDeposit.balance()) / 1e24);

//////////// 1. New Safe adddresses //////////////
expect(await contracts.pcvGuardianNew.isSafeAddress(addresses.ethToDaiLBPSwapper)).to.be.true;

///////////// 2. WETH LBP ////////////////
await validateLBPSetup(contracts, addresses, poolId);

///////////// 3. PSM Redeem ////////////////

// check PSM redeem fee
const psm = contracts.ethPSM;
expect(await psm.redeemFeeBasisPoints()).to.be.equal('60');

// Validate PSM_ADMIN_ROLE is under ROLE_ADMIN and that TribalCouncilTimelock has the role
expect(await core.hasRole(ethers.utils.id('PSM_ADMIN_ROLE'), addresses.tribalCouncilTimelock)).to.be.true;
expect(await core.getRoleAdmin(ethers.utils.id('PSM_ADMIN_ROLE'))).to.be.equal(ethers.utils.id('ROLE_ADMIN'));
};

const validateLBPSetup = async (contracts: NamedContracts, addresses: NamedAddresses, poolId: string) => {
const ethToDaiLBPSwapper = contracts.ethToDaiLBPSwapper;

expect(await ethToDaiLBPSwapper.doInvert()).to.be.equal(false);
expect(await ethToDaiLBPSwapper.isTimeStarted()).to.be.true;
expect(await ethToDaiLBPSwapper.tokenSpent()).to.equal(addresses.weth);
expect(await ethToDaiLBPSwapper.tokenReceived()).to.equal(addresses.dai);

// tokenSpent = WETH
// tokenReceived = DAI
// On BalancerVault, token[0] = WETH, token[1] = DAI
// Therefore, on LBPSwapper, assets[0] = WETH, assets[1] = DAI

// 2.1 Check oracle price
const price = (await ethToDaiLBPSwapper.readOracle())[0]; // DAI price in units of WETH
console.log('price: ', price);
expect(price).to.be.bignumber.at.least(ethers.constants.WeiPerEther.mul(1600)); // 1600e18
expect(price).to.be.bignumber.at.most(ethers.constants.WeiPerEther.mul(2200)); // 2200e18

// 2.2 Check relative price in pool

// Putting in 20,000 tokens of WETH, getting an amount of DAI back
const response = await ethToDaiLBPSwapper.getTokensIn(20000); // input is spent token balance, 100,000 WETH tokens
const amounts = response[1];

// DAI/WETH price * DAI amount * 5% ~= amount
expectApprox(price.mul(20000).mul(5).div(ethers.constants.WeiPerEther).div(100), amounts[1]); // DAI
expect(amounts[0]).to.be.bignumber.at.least(toBN(2_000_000)); // Make sure orcacle inversion is correct (i.e. not inverted)

expect(amounts[1]).to.be.bignumber.equal(ethers.BigNumber.from(20000)); // WETH

// 2.3 Check pool info
const poolTokens = await contracts.balancerVault.getPoolTokens(poolId);
// there should be 2.1M DAI in the pool
expect(poolTokens.tokens[0]).to.be.equal(contracts.dai.address); // this is DAI
expect(poolTokens.balances[0]).to.be.bignumber.at.least(ethers.constants.WeiPerEther.mul(2_000_000));
expect(poolTokens.balances[0]).to.be.bignumber.at.most(ethers.constants.WeiPerEther.mul(2_200_000));
// there should be 20k WETH in the pool
expect(poolTokens.tokens[1]).to.be.equal(contracts.weth.address); // this is WETH
expect(poolTokens.balances[1]).to.be.equal('20000000000000000000000');

// Validate that a swap can occur
const daiWhale = '0x5d3a536e4d6dbd6114cc1ead35777bab948e3643';
const daiWhaleSigner = await getImpersonatedSigner(daiWhale);
await forceEth(daiWhale);

const initialUserEthBalance = await contracts.weth.balanceOf(daiWhale);
const initialUserDaiBalance = await contracts.dai.balanceOf(daiWhale);

const amountIn = ethers.constants.WeiPerEther.mul(10_000);
await contracts.dai.connect(daiWhaleSigner).approve(addresses.balancerVault, amountIn);
await contracts.balancerVault.connect(daiWhaleSigner).swap(
{
poolId: poolId,
kind: 0,
assetIn: addresses.dai,
assetOut: addresses.weth,
amount: amountIn,
userData: '0x'
},
{
sender: daiWhale,
fromInternalBalance: false,
recipient: daiWhale,
toInternalBalance: false
},
0,
'10000000000000000000000'
);

const postUserEthBalance = await contracts.weth.balanceOf(daiWhale);
const postUserDaiBalance = await contracts.dai.balanceOf(daiWhale);

const daiSpent = initialUserDaiBalance.sub(postUserDaiBalance);
expect(daiSpent).to.be.bignumber.equal(amountIn);

const ethGained = postUserEthBalance.sub(initialUserEthBalance);
expect(ethGained).to.be.bignumber.at.least(ethers.constants.WeiPerEther.mul(4));
expect(ethGained).to.be.bignumber.at.most(ethers.constants.WeiPerEther.mul(6));

// Put in 10k DAI, got out ~5 WETH
// Implies price of $2000 per WETH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

console.log('DAI spent: ', daiSpent);
console.log('WETH gained: ', ethGained);

// Accelerate time and check ended
await time.increase(LBP_FREQUENCY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also fast forwarded time, performed a second swap and validated that the amountOut increased - to check that the weights were decreasing as expected. No strong feelings, it's a bit paranoid

expect(await ethToDaiLBPSwapper.isTimeEnded()).to.be.true;
};

export { deploy, setup, teardown, validate };
64 changes: 64 additions & 0 deletions proposals/description/eth_lbp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ProposalDescription } from '@custom-types/types';

const eth_lbp: ProposalDescription = {
title: 'FIP-110: Reinforce PCV via ETH LBP and tighter spread',
commands: [
{
target: 'pcvGuardianNew',
values: '0',
method: 'setSafeAddresses(address[])',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metacomment - there is a singular version of this function that is easier to call

arguments: [['{ethToDaiLBPSwapper}']],
description: 'Set the ethToDai LBP swapper to be guardian Safe addresses'
},
//////// ETH LBP ////////
{
target: 'pcvGuardianNew',
values: '0',
method: 'withdrawToSafeAddress(address,address,uint256,bool,bool)',
arguments: ['{aaveEthPCVDeposit}', '{ethToDaiLBPSwapper}', '20000000000000000000000', false, false],
description: 'Transfer WETH from Aave to the LBP swapper'
},
{
target: 'pcvGuardianNew',
values: '0',
method: 'withdrawToSafeAddress(address,address,uint256,bool,bool)',
arguments: ['{compoundDaiPCVDeposit}', '{ethToDaiLBPSwapper}', '3000000000000000000000000', false, false],
description: 'Withdraw 3M DAI from the CompoundPCVDeposit and transfer to the LBP pool'
},
// Swap and update CR oracle
{
target: 'ethToDaiLBPSwapper',
values: '0',
method: 'swap()',
Joeysantoro marked this conversation as resolved.
Show resolved Hide resolved
arguments: [],
description: 'Start the auction and override the current no-op auction'
},
{
target: 'collateralizationOracle',
values: '0',
method: 'addDeposit(address)',
arguments: ['{ethToDaiLensDai}'],
description: 'Add DAI swapper lens to the CR oracle'
},
{
target: 'collateralizationOracle',
values: '0',
method: 'addDeposit(address)',
arguments: ['{ethToDaiLensEth}'],
description: 'Add ETH swapper lens to the CR oracle'
},
////// Tighten ETH PSM //////////
{
target: 'ethPSM',
values: '0',
method: 'setRedeemFee(uint256)',
arguments: ['60'],
description: 'set PSM spread to 60'
}
],
description: `
FIP-110: Reinforce PCV via ETH LBP and tighter spread.
`
};

export default eth_lbp;
5 changes: 3 additions & 2 deletions protocol-configuration/collateralizationOracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const collateralizationAddresses = {
'turboFusePCVDeposit'
],
lusd: ['rariPool7LusdPCVDeposit', 'bammDeposit', 'lusdPSM'],
dai: ['compoundDaiPCVDepositWrapper', 'daiFixedPricePSM', 'dpiToDaiLensDai'],
dai: ['compoundDaiPCVDepositWrapper', 'daiFixedPricePSM', 'dpiToDaiLensDai', 'ethToDaiLensDai'],
usd: ['namedStaticPCVDepositWrapper', 'd3poolCurvePCVDeposit', 'd3poolConvexPCVDeposit'],
bal: ['balancerDepositBalWeth', 'balancerLensVeBalBal', 'balancerGaugeStaker'],
weth: [
Expand All @@ -26,7 +26,8 @@ const collateralizationAddresses = {
'wethDepositWrapper',
'balancerDepositFeiWeth',
'balancerLensBpt30Fei70Weth',
'balancerLensVeBalWeth'
'balancerLensVeBalWeth',
'ethToDaiLensEth'
],
dpi: ['dpiToDaiLensDpi'],
rai: ['raiPriceBoundPSM'],
Expand Down
Loading