Skip to content

Commit

Permalink
Flash Loan Feature (EIP-3156)and More
Browse files Browse the repository at this point in the history
- Added `flashLoanFee` and `flashLoanFeeProtocol` arguments to protocol instantiation logic
- Added `WithFlashLoan` contract (and supporting library) implementing `EIP-3156`. The protocol allows borrowers to take out collateral-free loan on any cover liquidity pool (or vault) by paying a small fee.
- Refactored library features to use the suffix `…internal`
- Checked each function to ensure that the usage of address arguments is not only intentional but also the trust issue is checked and documented using the `@suppress-address-trust-issue` decorator. The `address trust issue` check is performed by the `walk` script that displays potential attack vectors (related to addresses) which later could be boxed as contracts or interfaces.
- Checked each function to ensure that the usage of `ERC-20` variable(s) inside any function is properly documented using the `@suppress-malicious-erc20` decorate. This too is checked by the `walk` script warning developers about the possibility of `malicious ERC-20` behaviors. Furthermore, all ERC-20 interactions must be performed using `NTransferUtilV2` library that ensures transfers are properly done. This is important to avoid ERC-20 tokens that send less amount to the recipient while deducting the full specified amount at the sender’s end.
- To avoid overflow, removed the magic number `* 1 ether` on all places where it was being used as a divisor. Replaced `1 ether` with `ProtoUtilV1.PERCENTAGE_DIVISOR` which can later be customized to a smaller number.
- Updated the documentation
- Fixed tests and stories
- And more
  • Loading branch information
heyaibi committed Jan 7, 2022
1 parent dc250c4 commit 382c135
Show file tree
Hide file tree
Showing 131 changed files with 1,471 additions and 919 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"javascript.suggestionActions.enabled": false,
"[javascript]": {
"editor.formatOnSave": false,
"editor.defaultFormatter": "vscode.json-language-features"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[json]": {
"editor.quickSuggestions": {
Expand Down
17 changes: 12 additions & 5 deletions contracts/core/Protocol.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ contract Protocol is IProtocol, ProtoBase {
* @param values[6] governanceReporterCommission
* @param values[7] claimPlatformFee
* @param values[8] claimReporterCommission
* @param values[9] flashLoanFee
* @param values[10] flashLoanFeeProtocol
*/
function initialize(address[] memory addresses, uint256[] memory values) external override whenNotPaused {
// @suppress-reentrancy Can only be initialized once and only by a protocol member
Expand Down Expand Up @@ -65,6 +67,8 @@ contract Protocol is IProtocol, ProtoBase {
s.setUintByKey(ProtoUtilV1.NS_GOVERNANCE_REPORTER_COMMISSION, values[6]);
s.setUintByKey(ProtoUtilV1.NS_CLAIM_PLATFORM_FEE, values[7]);
s.setUintByKey(ProtoUtilV1.NS_CLAIM_REPORTER_COMMISSION, values[8]);
s.setUintByKey(ProtoUtilV1.NS_COVER_LIQUIDITY_FLASH_LOAN_FEE, values[9]);
s.setUintByKey(ProtoUtilV1.NS_COVER_LIQUIDITY_FLASH_LOAN_FEE_PROTOCOL, values[10]);

emit Initialized(addresses, values);
}
Expand All @@ -78,33 +82,36 @@ contract Protocol is IProtocol, ProtoBase {
ValidationLibV1.mustNotBePaused(s);
AccessControlLibV1.mustBeUpgradeAgent(s);

// @suppress-address-trust-issue Checked
s.upgradeContract(namespace, previous, current);
// @suppress-address-trust-issue Checked. Can only be assigned by an upgrade agent.
s.upgradeContractInternal(namespace, previous, current);
emit ContractUpgraded(namespace, previous, current);
}

function addContract(bytes32 namespace, address contractAddress) external override nonReentrant {
// @suppress-address-trust-issue Although the `contractAddress` can't be trusted, the upgrade admin has to check the contract code manually.
ValidationLibV1.mustNotBePaused(s);
AccessControlLibV1.mustBeUpgradeAgent(s);

s.addContract(namespace, contractAddress);
s.addContractInternal(namespace, contractAddress);
emit ContractAdded(namespace, contractAddress);
}

function removeMember(address member) external override nonReentrant {
// @suppress-address-trust-issue Can be trusted because this can only come from upgrade agents.
ProtoUtilV1.mustBeProtocolMember(s, member);
ValidationLibV1.mustNotBePaused(s);
AccessControlLibV1.mustBeUpgradeAgent(s);

s.removeMember(member);
s.removeMemberInternal(member);
emit MemberRemoved(member);
}

function addMember(address member) external override nonReentrant {
// @suppress-address-trust-issue Can be trusted because this can only come from upgrade agents.
ValidationLibV1.mustNotBePaused(s);
AccessControlLibV1.mustBeUpgradeAgent(s);

s.addMember(member);
s.addMemberInternal(member);
emit MemberAdded(member);
}

Expand Down
5 changes: 3 additions & 2 deletions contracts/core/Recoverable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ abstract contract Recoverable is ReentrancyGuard {
function recoverEther(address sendTo) external nonReentrant {
// @suppress-pausable Already implemented in BaseLibV1
// @suppress-acl Already implemented in BaseLibV1 --> mustBeRecoveryAgent
BaseLibV1.recoverEther(s, sendTo);
BaseLibV1.recoverEtherInternal(s, sendTo);
}

/**
Expand All @@ -33,6 +33,7 @@ abstract contract Recoverable is ReentrancyGuard {
function recoverToken(address token, address sendTo) external nonReentrant {
// @suppress-pausable Already implemented in BaseLibV1
// @suppress-acl Already implemented in BaseLibV1 --> mustBeRecoveryAgent
BaseLibV1.recoverToken(s, token, sendTo);
// @suppress-address-trust-issue Although the token can't be trusted, the recovery agent has to check the token code manually.
BaseLibV1.recoverTokenInternal(s, token, sendTo);
}
}
8 changes: 5 additions & 3 deletions contracts/core/claims/Processor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ contract Processor is IClaimsProcessor, Recoverable {
uint256 incidentDate,
uint256 amount
) external override nonReentrant {
// @suppress-pausable Already implemented in the function `validate`
// @suppress-acl Marking this as publicly accessible
// @suppress-pausable Already implemented in the function `validate`
// @suppress-address-trust-issue The `cxToken` address can be trusted because it is being checked in the function `validate`.
// @suppress-malicious-erc20 The function `NTransferUtilV2.ensureTransferFrom` checks if `cxToken` acts funny.

validate(cxToken, key, incidentDate);

Expand All @@ -58,9 +60,9 @@ contract Processor is IClaimsProcessor, Recoverable {
IVault vault = s.getVault(key);
address finalReporter = s.getReporter(key, incidentDate);

uint256 platformFee = (amount * s.getClaimPlatformFee()) / 1 ether;
uint256 platformFee = (amount * s.getClaimPlatformFee()) / ProtoUtilV1.PERCENTAGE_DIVISOR;
// slither-disable-next-line divide-before-multiply
uint256 reporterFee = (platformFee * s.getClaimReporterCommission()) / 1 ether;
uint256 reporterFee = (platformFee * s.getClaimReporterCommission()) / ProtoUtilV1.PERCENTAGE_DIVISOR;
uint256 claimed = amount - platformFee;

vault.transferGovernance(key, msg.sender, claimed);
Expand Down
7 changes: 4 additions & 3 deletions contracts/core/lifecycle/Cover.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ contract Cover is CoverBase {
) external override nonReentrant {
// @suppress-acl Can only be called by a whitelisted address
// @suppress-acl Marking this as publicly accessible
// @suppress-address-trust-issue The reassuranceToken can only be the stablecoin supported by the protocol for this version.
s.mustNotBePaused();
s.senderMustBeWhitelisted();

Expand All @@ -117,9 +118,10 @@ contract Cover is CoverBase {
if (initialLiquidity > 0) {
IVault vault = s.getVault(key);

s.getVault(key).addLiquidityInternal(key, msg.sender, initialLiquidity);
s.getVault(key).addLiquidityMemberOnly(key, msg.sender, initialLiquidity);

// Transfer liquidity only after minting the pods
// @suppress-malicious-erc20 This ERC-20 is a well-known address. Can only be set internally.
IERC20(s.getStablecoin()).ensureTransferFrom(msg.sender, address(vault), initialLiquidity);
}

Expand Down Expand Up @@ -156,8 +158,7 @@ contract Cover is CoverBase {
// Set reassurance token
s.setAddressByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_TOKEN, key, reassuranceToken);

// s.setUintByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_WEIGHT, key, 500000000 gwei); // Default 50% weight
s.setUintByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_WEIGHT, key, 1 ether); // 100% weight because it's a stablecoin
s.setUintByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_WEIGHT, key, ProtoUtilV1.PERCENTAGE_DIVISOR); // 100% weight because it's a stablecoin

// Set the fee charged during cover creation
s.setUintByKeys(ProtoUtilV1.NS_COVER_FEE_EARNING, key, fee);
Expand Down
1 change: 1 addition & 0 deletions contracts/core/lifecycle/CoverBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ abstract contract CoverBase is ICover, Recoverable {
*
*/
function initialize(address liquidityToken, bytes32 liquidityName) external override nonReentrant {
// @suppress-address-trust-issue liquidityToken This instance of liquidityToken can be trusted because of the ACL requirement.
s.mustNotBePaused();
AccessControlLibV1.mustBeCoverManager(s);

Expand Down
5 changes: 4 additions & 1 deletion contracts/core/lifecycle/CoverReassurance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ contract CoverReassurance is ICoverReassurance, Recoverable {

require(amount > 0, "Provide valid amount");

IERC20 reassuranceToken = IERC20(s.getAddressByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_TOKEN, key));
// IERC20 reassuranceToken = IERC20(s.getAddressByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_TOKEN, key));
// @suppress-malicious-erc20 This ERC-20 is a well-known address. Can only be set internally.
IERC20 reassuranceToken = IERC20(s.getStablecoin());

address vault = s.getReassuranceVault();

s.addUintByKeys(ProtoUtilV1.NS_COVER_REASSURANCE, key, amount);
Expand Down
8 changes: 6 additions & 2 deletions contracts/core/liquidity/Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.0;

import "./VaultBase.sol";
import "./WithFlashLoan.sol";

/**
* @title Cover Vault for Liquidity
Expand All @@ -17,7 +17,11 @@ import "./VaultBase.sol";
* - To protect liquidity providers from cover incidents, they can redeem upto 25% of the cover payouts through NPM provision.
* - To protect liquidity providers from cover incidents, they can redeem upto 25% of the cover payouts through `reassurance token` allocation.
*/
contract Vault is VaultBase {
contract Vault is WithFlashLoan {
using ProtoUtilV1 for IStore;
using ValidationLibV1 for IStore;
using VaultLibV1 for IStore;

constructor(
IStore store,
bytes32 coverKey,
Expand Down
8 changes: 5 additions & 3 deletions contracts/core/liquidity/VaultBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ abstract contract VaultBase is IVault, Recoverable, ERC20 {
* @param account Specify the account on behalf of which the liquidity is being added.
* @param amount Enter the amount of liquidity token to supply.
*/
function addLiquidityInternal(
function addLiquidityMemberOnly(
bytes32 coverKey,
address account,
uint256 amount
) external override nonReentrant {
// @suppress-acl Can only be accessed by the latest cover contract
// @suppress-address-trust-issue For more info, check the function `_addLiquidity`
s.mustNotBePaused();
s.mustBeValidCover(key);
s.callerMustBeCoverContract();
Expand Down Expand Up @@ -101,7 +102,7 @@ abstract contract VaultBase is IVault, Recoverable, ERC20 {
function removeLiquidity(bytes32 coverKey, uint256 podsToRedeem) external override nonReentrant {
s.mustNotBePaused();
require(coverKey == key, "Forbidden");
uint256 released = VaultLibV1.removeLiquidity(s, coverKey, address(this), lqt, podsToRedeem);
uint256 released = VaultLibV1.removeLiquidityInternal(s, coverKey, address(this), podsToRedeem);

emit PodsRedeemed(msg.sender, podsToRedeem, released);
}
Expand All @@ -118,9 +119,10 @@ abstract contract VaultBase is IVault, Recoverable, ERC20 {
uint256 amount,
bool initialLiquidity
) private {
// @suppress-address-trust-issue For more info, check the function `VaultLibV1.addLiquidityInternal`
require(coverKey == key, "Forbidden");

uint256 podsToMint = VaultLibV1.addLiquidity(s, coverKey, address(this), lqt, account, amount, initialLiquidity);
uint256 podsToMint = VaultLibV1.addLiquidityInternal(s, coverKey, address(this), lqt, account, amount, initialLiquidity);
super._mint(account, podsToMint);

emit PodsIssued(account, podsToMint, amount);
Expand Down
79 changes: 79 additions & 0 deletions contracts/core/liquidity/WithFlashLoan.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Neptune Mutual Protocol (https://neptunemutual.com)
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.0;

import "./VaultBase.sol";
import "openzeppelin-solidity/contracts/interfaces/IERC3156FlashLender.sol";

/**
* @title With Flash Loan Contract
* @dev WithFlashLoan contract implements `EIP-3156 Flash Loan`.
* Using flash loans, you can borrow up to the total available amount of
* the stablecoin liquidity available in this cover liquidity pool.
* You need to return back the borrowed amount + fee in the same transaction.
* The function `flashFee` enables you to check, in advance, fee that
* you need to pay to take out the loan.
*/
abstract contract WithFlashLoan is VaultBase, IERC3156FlashLender {
using ProtoUtilV1 for IStore;
using ValidationLibV1 for IStore;
using VaultLibV1 for IStore;
using NTransferUtilV2 for IERC20;

/**
* @dev The fee to be charged for a given loan.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @return The amount of `token` to be charged for the loan, on top of the returned principal.
*/
function flashFee(address token, uint256 amount) external view override returns (uint256) {
(uint256 fee, ) = s.getFlashFeeInternal(token, amount);
return fee;
}

/**
* @dev The amount of currency available to be lent.
* @param token The loan currency.
* @return The amount of `token` that can be borrowed.
*/
function maxFlashLoan(address token) external view override returns (uint256) {
return s.getMaxFlashLoanInternal(token);
}

/**
* @dev Initiate a flash loan.
* @param receiver The receiver of the tokens in the loan, and the receiver of the callback.
* @param token The loan currency.
* @param amount The amount of tokens lent.
* @param data Arbitrary data structure, intended to contain user-defined parameters.
*/
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external override nonReentrant returns (bool) {
// @suppress-address-trust-issue The instance of `token` can be trusted because we're ensuring it matches with the protocol stablecoin address.
IERC20 stablecoin = IERC20(s.getStablecoin());
(uint256 fee, uint256 protocolFee) = s.getFlashFeeInternal(token, amount);
uint256 previousBalance = stablecoin.balanceOf(address(this));

s.mustNotBePaused();

require(address(stablecoin) == token, "Unknown token");
require(amount > 0, "Loan too small");
require(fee > 0, "Fee too little");
require(previousBalance >= amount, "Balance insufficient");

stablecoin.ensureTransfer(address(receiver), amount);
require(receiver.onFlashLoan(msg.sender, token, amount, fee, data) == keccak256("ERC3156FlashBorrower.onFlashLoan"), "IERC3156: Callback failed");
stablecoin.ensureTransferFrom(address(receiver), address(this), amount + fee);
stablecoin.ensureTransfer(s.getTreasury(), protocolFee);

uint256 finalBalance = stablecoin.balanceOf(address(this));
require(finalBalance >= previousBalance + fee, "Access is denied");

emit FlashLoanBorrowed(address(this), address(receiver), token, amount, fee);
return true;
}
}
12 changes: 8 additions & 4 deletions contracts/core/policy/Policy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ contract Policy is IPolicy, Recoverable {

ICxTokenFactory factory = s.getCxTokenFactory();
cxToken = factory.deploy(s, key, expiryDate);
s.addMember(cxToken);
s.addMemberInternal(cxToken);

return ICxToken(cxToken);
}
Expand Down Expand Up @@ -229,13 +229,17 @@ contract Policy is IPolicy, Recoverable {
require(values[0] - values[1] > amountToCover, "Insufficient fund");

// UTILIZATION RATIO = COVER_COMMITMENT / AMOUNT_IN_COVER_POOL
utilizationRatio = (1 ether * values[1]) / values[0];
utilizationRatio = (ProtoUtilV1.PERCENTAGE_DIVISOR * values[1]) / values[0];

// TOTAL AVAILABLE LIQUIDITY = AMOUNT_IN_COVER_POOL - COVER_COMMITMENT + (NEP_REWARD_POOL_SUPPORT * NEP_PRICE) + (REASSURANCE_POOL_SUPPORT * REASSURANCE_TOKEN_PRICE * REASSURANCE_POOL_WEIGHT)
totalAvailableLiquidity = values[0] - values[1] + ((values[2] * values[3]) / 1 ether) + ((values[4] * values[5] * values[6]) / (1 ether * 1 ether));
totalAvailableLiquidity =
values[0] -
values[1] +
((values[2] * values[3]) / ProtoUtilV1.PERCENTAGE_DIVISOR) +
((values[4] * values[5] * values[6]) / (ProtoUtilV1.PERCENTAGE_DIVISOR * ProtoUtilV1.PERCENTAGE_DIVISOR));

// COVER RATIO = UTILIZATION_RATIO + COVER_DURATION * AMOUNT_TO_COVER / AVAILABLE_LIQUIDITY
coverRatio = utilizationRatio + ((1 ether * coverDuration * amountToCover) / totalAvailableLiquidity);
coverRatio = utilizationRatio + ((ProtoUtilV1.PERCENTAGE_DIVISOR * coverDuration * amountToCover) / totalAvailableLiquidity);

rate = _getCoverFeeRate(floor, ceiling, coverRatio);
fee = (amountToCover * rate * coverDuration) / (12 ether);
Expand Down
1 change: 1 addition & 0 deletions contracts/core/store/StoreBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ abstract contract StoreBase is IStore, Pausable, Ownable {
* @param token IERC-20 The address of the token contract
*/
function recoverToken(address token, address sendTo) external onlyOwner {
// @suppress-address-trust-issue Although the token can't be trusted, the owner has to check the token code manually.
IERC20 erc20 = IERC20(token);

uint256 balance = erc20.balanceOf(address(this));
Expand Down
3 changes: 2 additions & 1 deletion contracts/interfaces/IVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ interface IVault is IMember, IERC20 {
event PodsIssued(address indexed account, uint256 issued, uint256 liquidityAdded);
event PodsRedeemed(address indexed account, uint256 redeemed, uint256 liquidityReleased);
event MinLiquidityPeriodSet(uint256 previous, uint256 current);
event FlashLoanBorrowed(address indexed lender, address indexed borrower, address indexed stablecoin, uint256 amount, uint256 fee);

/**
* @dev Adds liquidity to the specified cover contract
* @param coverKey Enter the cover key
* @param account Specify the account on behalf of which the liquidity is being added.
* @param amount Enter the amount of liquidity token to supply.
*/
function addLiquidityInternal(
function addLiquidityMemberOnly(
bytes32 coverKey,
address account,
uint256 amount
Expand Down
5 changes: 3 additions & 2 deletions contracts/libraries/BaseLibV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ library BaseLibV1 {
* On success, no event is emitted because the recovery feature does
* not have any significance in the SDK or the UI.
*/
function recoverEther(IStore s, address sendTo) external {
function recoverEtherInternal(IStore s, address sendTo) external {
s.mustNotBePaused();
AccessControlLibV1.mustBeRecoveryAgent(s);

Expand All @@ -30,11 +30,12 @@ library BaseLibV1 {
* not have any significance in the SDK or the UI.
* @param token IERC-20 The address of the token contract
*/
function recoverToken(
function recoverTokenInternal(
IStore s,
address token,
address sendTo
) external {
// @suppress-address-trust-issue Although the token can't be trusted, the recovery agent has to check the token code manually.
s.mustNotBePaused();
AccessControlLibV1.mustBeRecoveryAgent(s);

Expand Down
6 changes: 3 additions & 3 deletions contracts/libraries/CoverUtilV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ library CoverUtilV1 {
* 4 - claimable, claims accepted for payout
*
*/
function getCoverStatus(IStore s, bytes32 key) public view returns (CoverStatus) {
function getCoverStatus(IStore s, bytes32 key) external view returns (CoverStatus) {
return CoverStatus(getStatus(s, key));
}

Expand All @@ -81,9 +81,9 @@ library CoverUtilV1 {
_values[0] = s.getUintByKeys(ProtoUtilV1.NS_COVER_LIQUIDITY, key);
_values[1] = s.getUintByKeys(ProtoUtilV1.NS_COVER_LIQUIDITY_COMMITTED, key); // <-- Todo: liquidity commitment should expire as policies expire
_values[2] = s.getUintByKeys(ProtoUtilV1.NS_COVER_PROVISION, key);
_values[3] = discovery.getTokenPriceInStableCoin(address(s.npmToken()), 1 ether);
_values[3] = discovery.getTokenPriceInStableCoin(address(s.npmToken()), ProtoUtilV1.PERCENTAGE_DIVISOR);
_values[4] = s.getUintByKeys(ProtoUtilV1.NS_COVER_REASSURANCE, key);
_values[5] = discovery.getTokenPriceInStableCoin(address(s.getAddressByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_TOKEN, key)), 1 ether);
_values[5] = discovery.getTokenPriceInStableCoin(address(s.getAddressByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_TOKEN, key)), ProtoUtilV1.PERCENTAGE_DIVISOR);
_values[6] = s.getUintByKeys(ProtoUtilV1.NS_COVER_REASSURANCE_WEIGHT, key);
}

Expand Down
Loading

0 comments on commit 382c135

Please sign in to comment.