Pensify is a secure, non-custodial, no-loss and no-risk Pension Fund built on Ethereum blockchain. By using Robo-Advisor for Yield (RAY) from Staked.US, Fund constantly generates interest from different DeFi protocols - Compound, Aave, dYdX, Fulcrum (latest is turned off atm), MCD, DSR. Members can also use Flash Loans to earn additional income via a browser bot for automatic arbitrage between Uniswap and Balancer pools. The fund is built using AkropolisOS framework, which allows automated liquidity provision enabled by the bonding curve, treasury management & automated yield rebalancing.
- We used AkropolisOS framework (https://akropolis.io) to build a basic architecture for Pensify. It is based on OpenZeppelin (http://openzeppelin.io) and allows automated liquidity provision enabled by the bonding curve, treasury management & automated yield rebalancing.
- To enable mobile support, we used Portis Wallet (https://www.portis.io). It provides secure storage and access to Pensify from any device.
- We use Uniswap (https://uniswap.exchange) and Balancer (https://balancer.finance) protocols as a part of arbitrage strategies for fund members. They can earn additional income by utilizing Flash Loans and performing arbitrage between Uniswap and Balancer.
- We use Compound and Aave (https://compound.finance) and Aave (https://aave.com) as an interest source through rebalancer - Robo Yield Advisor (https://staked.us/v/robo-advisor-yield/) from Staked.Us. It allows us to accumulate interest on all Pensify funds.
Mainnet deployment (https://pensionfund.fi)
- DAI:
0x6b175474e89094c44da98b954eedeac495271d0f
- cDAI: not used
- RAY Storage:
0x446711e5ed3013743e40342a0462fbdc437cd43f
- Pool:
0x3501d2c95F8dB9A94E0f0BCD15E2a440C71ceaE4
- AccessModule:
0x3f2ced4322ecfd1a77fc972bd6d690cf632ba09c
- PToken:
0x764112eCFFDdB111f78e9475d70010fD1120257f
- CurveModule:
0xa6d9d61c6637e8d1ab1f535baabb53e756559cdc
- CompoundModule: not deployed
- RAYModule:
0xEEbaf85E5452F11e33e059ADb3F2F10E748a3562
- FundsModule:
0x1dEA32aAd5Ef531538CdC7eab515072aBc65d855
- PensionFundModule:
0x23b1Fb463a87815F6f8714bc4af9Ce8214C4c748
- FlashLoansModule:
0x7cD7833930E7fb43Fc4F221eBfFE3eFAE39D1442
- ArbitrageModule:
0x6E2CFb462D04b2385fE5d1D16A6e0A8154fd201e
Testnet (Kovan) deployment (https://kovan.pensionfund.fi)
- DAI:
0x4f96fe3b7a6cf9725f59d353f723c1bdb64ca6aa
- cDAI:
0xe7bc397dbd069fc7d0109c0636d06888bb50668c
- RAY Storage: not used
- Pool:
0xBc4C64C8F5838C4A7e10Ac9bB0b606D3AD4c8809
- AccessModule:
0x790C6cAB44C0ff8311E5F501d36b57B2aD18e9C9
- PToken:
0xcC64F821A6C32884C0648C12E62585FdBC7bA082
- CurveModule:
0xBA9d498AA8d650b9ce38D6cE5B0d6539d254A3e8
- CompoundModule:
0xDc6b5507647137B663fe81C4aBA6912a88eF9F73
- RAYModule: not deployed
- FundsModule:
0x29518F102cC748d30178e1fB6215f2BEF4a85b86
- PensionFundModule:
0x03843c8a5b7A6c4F563CF5514E53286A7f934ea0
- FlashLoansModule:
0x310879fEf4e301425336eBC2f58C29bd5127d174
- ArbitrageModule:
0x220F8d93889fD51528b7b119FF7C9a10149EbCf2
- Address of liquidity token (
LToken.address
) - Address of cDAI contract (
cDAI.address
)
- Initialize OpenZeppelin project & add modules
npx oz init
npx oz add Pool AccessModule PToken CompoundModule DefiFundsModule CurveModule LiquidityModule LoanLimitsModule LoanProposalsModule LoanModule
- Deploy & initialize Pool
npx oz create Pool --network kovan --init
- Save address of the pool (
Pool.address
)
- Deploy modules
npx oz create AccessModule --network kovan --init "initialize(address _pool)" --args Pool.address
npx oz create PToken --network kovan --init "initialize(address _pool)" --args Pool.address
npx oz create CurveModule --network kovan --init "initialize(address _pool)" --args Pool.address
npx oz create CompoundModule --network kovan --init "initialize(address _pool)" --args Pool.address
npx oz create DefiFundsModule --network kovan --init "initialize(address _pool)" --args Pool.address
npx oz create PensionFundModule --network kovan --init "initialize(address _pool)" --args Pool.address
npx oz create FlashLoansModule --network kovan --init "initialize(address _pool)" --args Pool.address
npx oz create ArbitrageModule --network kovan --init "initialize(address _pool)" --args Pool.address
- Save address of each module:
AccessModule.address
,PToken.address
,CurveModule.address
,CompoundModule.address
,DefiFundsModule.address
,LiquidityModule.address
,FlashLoansModule.address
,ArbitrageModule.address
- Register external contracts in Pool
npx oz send-tx --to Pool.address --network kovan --method set --args "ltoken, LToken.address, false"
npx oz send-tx --to Pool.address --network kovan --method set --args "cdai, cDAI.address, false"
- Register modules in pool
npx oz send-tx --to Pool.address --network kovan --method set --args "access, AccessModule.address, false"
npx oz send-tx --to Pool.address --network kovan --method set --args "ptoken, PToken.address, false"
npx oz send-tx --to Pool.address --network kovan --method set --args "defi, CompoundModule.address, false"
npx oz send-tx --to Pool.address --network kovan --method set --args "curve, CurveModule.address, false"
npx oz send-tx --to Pool.address --network kovan --method set --args "funds, DefiFundsModule.address, false"
npx oz send-tx --to Pool.address --network kovan --method set --args "liquidity, PensionFundModule.address
npx oz send-tx --to Pool.address --network kovan --method set --args "flashloans, FlashLoansModule.address, false
npx oz send-tx --to Pool.address --network kovan --method set --args "arbitrage, ArbitrageModule.address, false"
- Configure modules
npx oz send-tx --to DefiFundsModule.address --network kovan --method addFundsOperator --args PensionFundModule.address
npx oz send-tx --to DefiFundsModule.address --network kovan --method addFundsOperator --args FlashLoansModule.address
npx oz send-tx --to PToken.address --network kovan --method addMinter --args DefiFundsModule.address
npx oz send-tx --to CompoundModule.address --network kovan --method addDefiOperator --args DefiFundsModule.address
- Configure fee (optional)
npx oz send-tx --to CurveModule.address --network kovan --method setWithdrawFee --args 5
npx oz send-tx --to FlashLoansModule.address --network kovan --method setFee --args 100000000000000
lAmount
: Deposit amount, DAI
- All contracts are deployed
- Call
FundsModule.calculatePoolEnter(lAmount)
to determine expected PTK amount (pAmount
) - Determine minimum acceptable amount of PTK
pAmountMin <= pAmount
, which user expects to get when depositlAmount
of DAI. Zero value is allowed. - Call
LToken.approve(FundsModule.address, lAmount)
to allow exchange - Call
LiquidityModule.deposit(lAmount, pAmountMin)
to execute exchange
pAmount
: Withdraw amount, PTK
- Available liquidity
LToken.balanceOf(FundsModule.address)
is greater than expected amount of DAI - User has enough PTK:
PToken.balanceOf(userAddress) >= pAmount
- Call
FundsModule.calculatePoolExitInverse(pAmount)
to determine expected amount of DAI (lAmount
). The response has 3 values, use the second one. - Determine minimum acceptable amount
lAmountMin <= lAmount
of DAI , which user expects to get when depositpAmount
of PTK. Zero value is allowed. - Call
PToken.approve(FundsModule.address, pAmount)
to allow exchange - Call
LiquidityModule.withdraw(pAmount, lAmountMin)
to execute exchange
debtLAmount
: Loan amount, DAIinterest
: Interest rate, percentspAmountMax
: Maximal amount of PTK to use as borrower's own pledgedescriptionHash
: Hash of loan description stored in Swarm
- User has enough PTK:
PToken.balanceOf(userAddress) >= pAmount
- Call
FundsModule.calculatePoolExitInverse(pAmount)
to determine expected pledge in DAI (lAmount
). The response has 3 values, use the first one. - Determine minimum acceptable amount
lAmountMin <= lAmount
of DAI, which user expects to lock as a pledge, sendingpAmount
of PTK. Zero value is allowed. - Call
PToken.approve(FundsModule.address, pAmount)
to allow operation. - Call
LoanModule.createDebtProposal(debtLAmount, interest, pAmountMax, descriptionHash)
to create loan proposal.
- Proposal index:
proposalIndex
from eventDebtProposalCreated
.
- Loan proposal identifiers:
borrower
Address of borrowerproposal
Proposal index
pAmount
Pledge amount, PTK
- Loan proposal created
- Loan proposal not yet executed
- Loan proposal is not yet fully filled:
LoanModule.getRequiredPledge(borrower, proposal) > 0
- User has enough PTK:
PToken.balanceOf(userAddress) >= pAmount
- Call
FundsModule.calculatePoolExitInverse(pAmount)
to determine expected pledge in DAI (lAmount
). The response has 3 values, use the first one. - Determine minimum acceptable amount
lAmountMin <= lAmount
of DAI, which user expects to lock as a pledge, sendingpAmount
of PTK. Zero value is allowed. - Call
PToken.approve(FundsModule.address, pAmount)
to allow operation. - Call
LoanModule.addPledge(borrower, proposal, pAmount, lAmountMin)
to execute operation.
- Loan proposal identifiers:
borrower
Address of borrowerproposal
Proposal index
pAmount
Amount to withdraw, PTK
- Loan proposal created
- Loan proposal not yet executed
- User pledge amount >=
pAmount
- Call
LoanModule.withdrawPledge(borrower, proposal, pAmount)
to execute operation.
proposal
Proposal index
- Loan proposal created, user (transaction sender) is the
borrower
- Loan proposal not yet executed
- Loan proposal is fully funded:
LoanModule.getRequiredPledge(borrower, proposal) == 0
- Pool has enough liquidity
- Call
LoanModule.executeDebtProposal(proposal)
to execute operation.
- Loan index:
debtIdx
from eventDebtProposalExecuted
.
debt
Loan indexlAmount
Repayable amount, DAI
- User (transaction sender) is the borrower
- Loan is not yet fully repaid
- Call
LToken.approve(FundsModule.address, lAmount)
to allow operation. - Call
LoanModule.repay(debt, lAmount)
to execute operation.
When borrower repays some part of his loan, he uses some PTK (either from his balance or minted when he sends DAI to the pool). This PTKs are distributed to supporters, proportionally to the part of the loan they covered. The borrower himself also covered half of the loan, and his part is distributed over the whole pool. All users of the pool receive part of this distributions proportional to the amount of PTK they hold on their balance and in loan proposals, PTK locked as collateral for loans is not counted.
When you need to distribute some amount of tokens over all token holders one's first straight-forward idea might be to iterate through all token holders, check their balance and increase it by their part of the distribution. Unfortunately, this approach can hardly be used in Ethereum blockchain. All operations in EVM cost some gas. If we have a lot of token holders, gas cost for iteration through all may be higher than a gas limit for transaction (which is currently equal to gas limit for block). Instead, during distribution we just store amount of PTK to be distributed and current amount of all PTK qualified for distribution. And user balance is only updated by separate request or when it is going to be changed by transfer, mint or burn. During this "lazy" update we go through all distributions occured between previous and current update. Now, one may ask what if there is too much distributions occurred in the pool between this updated and the gas usage to iterate through all of them is too high again? Obvious solution would be to allow split such transaction to several smaller ones, and we've implemented this approach. But we also decided to aggregate all distributions during a day. This way we can protect ourself from dust attacks, when somebody may do a lot of small repays which cause a lot of small distributions. When a distribution request is received by PToken we check if it's time to actually create new distribution. If it's not, we just add distribution amount to the accumulator. When time comes (and this condition is also checked by transfers, mints and burns), actual distribution is created using accumulated amount of PTK and total supply of qualified PTK.
Defi module transfers funds to some underlying protocol, Compound in current version. Exchange rate of DAI to Compound DAI is icreased over time. So while amount of Compound DAI stays same, amount of underlying DAI available is continiously increased. During distributions Defi module calculates this additional ammount, so that PTK holders can widhraw their share at any time.
Defi module is configured to create distributions once a day. It stores time of next distribution and when time comes, any change of PTK balance or withdraw request will trigger a new distribution. With this distribution event Defi module stores how many additional DAI it can distribute, current balances of DAI and PTK. When one decides to withdraw (claim) his share of this additional DAI, Defi module iterates through all unclaimed distributions and calculates user's share of that distribution accroding to user's PTK balance and total amount of PTK at that time.