Skip to content

Commit

Permalink
Merge pull request #103 from primitivefinance/fix/engine-lending
Browse files Browse the repository at this point in the history
Fix/engine lending
  • Loading branch information
Alexangelj committed Jun 28, 2021
2 parents d73ed6d + 33cb605 commit 43ef720
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 106 deletions.
102 changes: 57 additions & 45 deletions contracts/PrimitiveEngine.sol
Expand Up @@ -351,41 +351,42 @@ contract PrimitiveEngine is IPrimitiveEngine {
/// @inheritdoc IPrimitiveEngineActions
function borrow(
bytes32 poolId,
address recipient,
uint256 delLiquidity,
uint256 maxPremium,
bytes calldata data
) external override lock {
) external override lock returns (uint256 premium) {
// Source: Convex Payoff Approimation. https://stanford.edu/~guillean/papers/cfmm-lending.pdf. Section 5
Reserve.Data storage reserve = reserves[poolId];
{
uint256 delLiquidity = delLiquidity;
require(reserve.float >= delLiquidity && delLiquidity > 0, "Insufficient float"); // fail early if not enough float to borrow

uint256 resLiquidity = reserve.liquidity; // global liquidity balance
uint256 delRisky = (delLiquidity * reserve.reserveRisky) / resLiquidity; // amount of risky asset
uint256 delStable = (delLiquidity * reserve.reserveStable) / resLiquidity; // amount of stable asset

{
uint256 preRisky = IERC20(risky).balanceOf(address(this));
uint256 preStable = IERC20(stable).balanceOf(address(this));

// trigger callback before position debt is increased, so liquidity can be removed
IERC20(stable).safeTransfer(msg.sender, delStable);
IPrimitiveLendingCallback(msg.sender).borrowCallback(delLiquidity, delRisky, delStable, data); // trigger the callback so we can remove liquidity
positions.borrow(poolId, delLiquidity); // increase liquidity + debt
// fails if risky asset balance is less than borrowed `delLiquidity`
reserve.remove(delRisky, delStable, delLiquidity, _blockTimestamp());
reserve.borrowFloat(delLiquidity);

uint256 postRisky = IERC20(risky).balanceOf(address(this));
uint256 postRiskless = IERC20(stable).balanceOf(address(this));

require(postRisky >= preRisky + (delLiquidity - delRisky), "IRY");
require(postRiskless >= preStable - delStable, "IRL");
}
require(reserve.float >= delLiquidity && delLiquidity > 0, "Insufficient float"); // fail early if not enough float to borrow

emit Borrowed(recipient, poolId, delLiquidity, maxPremium);
uint256 resLiquidity = reserve.liquidity; // global liquidity balance
uint256 delRisky = (delLiquidity * reserve.reserveRisky) / resLiquidity; // amount of risky asset
uint256 delStable = (delLiquidity * reserve.reserveStable) / resLiquidity; // amount of stable asset

{
// Balances before position creation
uint256 preRisky = IERC20(risky).balanceOf(address(this));
uint256 preStable = IERC20(stable).balanceOf(address(this));
// 0. Update position of `msg.sender` with `delLiquidity` units of debt and `risky` tokens
positions.borrow(poolId, delLiquidity);
// 1. Borrow `delLiquidity`: Reduce global reserve float, increase global debt
reserve.borrowFloat(delLiquidity);
// 2. Remove liquidity: Releases `risky` and `stable` tokens
reserve.remove(delRisky, delStable, delLiquidity, _blockTimestamp());
// 3. Sell `stable` tokens for `risky` tokens, agnostically, within the callback
IERC20(stable).safeTransfer(msg.sender, delStable); // transfer the stable tokens of the liquidity out to the `msg.sender`
IPrimitiveLendingCallback(msg.sender).borrowCallback(delLiquidity, delRisky, delStable, data);
// Check price impact tolerance
premium = delLiquidity - delRisky;
require(maxPremium >= premium, "Max");
// Check balances after position creation
uint256 postRisky = IERC20(risky).balanceOf(address(this));
uint256 postStable = IERC20(stable).balanceOf(address(this));
require(postRisky >= preRisky + premium, "IRY");
require(postStable >= preStable - delStable, "IRL");
}

emit Borrowed(msg.sender, poolId, delLiquidity, maxPremium);
}

/// @inheritdoc IPrimitiveEngineActions
Expand All @@ -396,30 +397,41 @@ contract PrimitiveEngine is IPrimitiveEngine {
uint256 delLiquidity,
bool fromMargin,
bytes calldata data
) external override lock returns (uint256 deltaRisky, uint256 deltaStable) {
)
external
override
lock
returns (
uint256 delRisky,
uint256 delStable,
uint256 premium
)
{
Reserve.Data storage reserve = reserves[poolId];
Position.Data storage position = positions.fetch(owner, poolId);
Margin.Data storage margin = margins[owner];

require(reserve.debt >= delLiquidity && position.liquidity >= delLiquidity, "ID");
// There is `delLiquidity` units of debt, which must be repaid using `delLiquidity` risky tokens.
position.repay(delLiquidity); // must have an open position, releases position.debt of risky
delRisky = (delLiquidity * reserve.reserveRisky) / reserve.liquidity; // amount of risky required to mint LP
delStable = (delLiquidity * reserve.reserveStable) / reserve.liquidity; // amount of stable required to mint LP
require(delRisky * delStable > 0, "Deltas are 0"); // fail early if 0 amounts

deltaRisky = (delLiquidity * reserve.reserveRisky) / reserve.liquidity;
deltaStable = (delLiquidity * reserve.reserveStable) / reserve.liquidity;
premium = delLiquidity - delRisky; // amount of excess risky, used to pay for stable side

// Update state
reserve.allocate(delRisky, delStable, delLiquidity, _blockTimestamp());
reserve.repayFloat(delLiquidity);
// Balances prior to callback/transfers
uint256 preRisky = IERC20(risky).balanceOf(address(this));
uint256 preStable = IERC20(stable).balanceOf(address(this));
if (fromMargin) {
margins.withdraw(delLiquidity - deltaRisky, deltaStable); // reduce margin balance
reserve.allocate(deltaRisky, deltaStable, delLiquidity, _blockTimestamp()); // increase reserve liquidity
position.repay(delLiquidity); // reduce position debt
margins.withdraw(0, delStable); // pay stables from margin balance
margin.deposit(premium, 0); // receive remainder `premium` of risky to margin
} else {
uint256 preStable = IERC20(stable).balanceOf(address(this));
IPrimitiveLendingCallback(msg.sender).repayFromExternalCallback(deltaStable, data);

require(IERC20(stable).balanceOf(address(this)) >= preStable + deltaStable, "IS");

reserve.allocate(deltaRisky, deltaStable, delLiquidity, _blockTimestamp());
reserve.repayFloat(delLiquidity);
position.repay(delLiquidity);
margin.deposit(delLiquidity - deltaRisky, uint256(0));
IERC20(risky).safeTransfer(msg.sender, premium); // This is a concerning line of code!
IPrimitiveLendingCallback(msg.sender).repayFromExternalCallback(delStable, data);
require(IERC20(stable).balanceOf(address(this)) >= preStable + delStable, "IS"); // fails if stable is not paid
}

emit Repaid(owner, poolId, delLiquidity);
Expand Down
16 changes: 12 additions & 4 deletions contracts/interfaces/engine/IPrimitiveEngineActions.sol
Expand Up @@ -116,29 +116,37 @@ interface IPrimitiveEngineActions {

/// @notice Increases the `msg.sender`'s position's liquidity value and also adds the same to the debt value.
/// @param poolId Keccak hash of the option parameters of a curve to interact with
/// @param owner Position owner to grant the borrowed liquidity shares
/// @param delLiquidity Amount of liquidity to borrow and add as debt
/// @param maxPremium Max amount of `premium` that can be collected from the `msg.sender` to collateralize the position
/// @param data Arbitrary data that is passed to the borrowCallback function
/// @return premium Price paid to open position
function borrow(
bytes32 poolId,
address owner,
uint256 delLiquidity,
uint256 maxPremium,
bytes calldata data
) external;
) external returns (uint256 premium);

/// @notice Reduces the `msg.sender`'s position's liquidity value and also reduces the same to the debt value.
/// @param poolId Keccak hash of the option parameters of a curve to interact with
/// @param owner Position owner to grant the borrowed liquidity shares
/// @param delLiquidity Amount of liquidity to borrow and add as debt
/// @param fromMargin Whether the `msg.sender` uses their margin balance, or must send tokens
/// @param data Arbitrary data that is passed to the repayCallback function
/// @return delRisky Amount of risky tokens allocated as liquidity to pay debt
/// delStable Amount of stable tokens allocated as liquidity to pay debt
/// premium Amount of risky tokens paid to the `owner`'s margin account
function repay(
bytes32 poolId,
address owner,
uint256 delLiquidity,
bool fromMargin,
bytes calldata data
) external returns (uint256, uint256);
)
external
returns (
uint256 delRisky,
uint256 delStable,
uint256 premium
);
}
22 changes: 9 additions & 13 deletions contracts/interfaces/engine/IPrimitiveEngineView.sol
Expand Up @@ -19,16 +19,16 @@ interface IPrimitiveEngineView {
) external view returns (int128 reserveOfToken);

/// @notice Uses the trading function to calc the invariant using token reserve values
/// @param poolId The hashed pool Id
/// @param postR1 Amount of risky tokens in the pool's reserves
/// @param postR2 Amount of stable tokens in the pool's reserves
/// @param postLiquidity Total supply of liquidity shares for the pool
/// @param poolId The hashed pool Id
/// @param resRisky Amount of risky tokens in the pool's reserves
/// @param resStable Amount of stable tokens in the pool's reserves
/// @param resLiquidity Total supply of liquidity shares for the pool
/// @return Invariant calculated (which should be near 0)
function calcInvariant(
bytes32 poolId,
uint256 postR1,
uint256 postR2,
uint256 postLiquidity
uint256 resRisky,
uint256 resStable,
uint256 resLiquidity
) external view returns (int128);

/// @notice Fetches the current invariant based on risky and stable token reserves of pool with `poolId`
Expand Down Expand Up @@ -91,17 +91,13 @@ interface IPrimitiveEngineView {

/// @notice Fetches Position data struct using a position id
/// @param posId Position id
/// @return balanceRisky Risky balance of the position debt
/// balanceStable Stable balance of the position debt
/// float Liquidity shares that are marked for loans
/// @return float Liquidity shares that are marked for loans
/// liquidity Liquidity shares in the position
/// debt Liquidity shares in debt, must be repaid
/// debt Liquidity shares in debt, must be repaid, also equal to risky balance of position
function positions(bytes32 posId)
external
view
returns (
uint128 balanceRisky,
uint128 balanceStable,
uint128 float,
uint128 liquidity,
uint128 debt
Expand Down
15 changes: 5 additions & 10 deletions contracts/libraries/Position.sol
Expand Up @@ -7,17 +7,14 @@ pragma abicoder v2;
/// @dev This library is a generalized position data structure for any engine.

import "./SafeCast.sol";
import "hardhat/console.sol";

library Position {
using SafeCast for uint256;

struct Data {
uint128 balanceRisky; // Balance of risky asset
uint128 balanceStable; // Balance of stable asset
uint128 float; // Balance of loaned liquidity
uint128 liquidity; // Balance of liquidity, which is negative if a debt exists
uint128 debt; // Balance of liquidity debt that must be paid back
uint128 debt; // Balance of liquidity debt that must be paid back, also balance of risky in position
}

/// @notice An Engine's mapping of position Ids to Data structs can be used to fetch any position.
Expand All @@ -35,6 +32,7 @@ library Position {

/// @notice Add to the balance of liquidity
function allocate(Data storage position, uint256 delLiquidity) internal returns (Data storage) {
require(position.debt == 0, "Debt");
position.liquidity += delLiquidity.toUint128();
return position;
}
Expand All @@ -57,10 +55,8 @@ library Position {
uint256 delLiquidity
) internal returns (Data storage) {
Data storage position = fetch(positions, msg.sender, poolId);
uint128 liquidity = position.liquidity;
require(liquidity == 0, "Must borrow from 0");
require(position.liquidity == 0, "Must borrow from 0");
position.debt += delLiquidity.toUint128(); // add the debt post position manipulation
position.balanceRisky += delLiquidity.toUint128();
return position;
}

Expand All @@ -87,10 +83,9 @@ library Position {
return position;
}

/// @notice Reduces `delLiquidity` of position.debt by reducing `delLiquidity` of position.liquidity
/// @notice Reduces `delLiquidity` of position.debt
function repay(Data storage position, uint256 delLiquidity) internal returns (Data storage) {
position.liquidity -= delLiquidity.toUint128();
// FIX: Contract too large, position.debt -= delLiquidity.toUint128();
position.debt -= delLiquidity.toUint128();
return position;
}

Expand Down
52 changes: 50 additions & 2 deletions contracts/test/engine/EngineBorrow.sol
Expand Up @@ -10,6 +10,8 @@ contract EngineBorrow {
address public stable;
address public CALLER;

uint256 public dontPay = 1;

constructor() {}

function initialize(
Expand All @@ -29,7 +31,30 @@ contract EngineBorrow {
bytes calldata data
) public {
CALLER = msg.sender;
IPrimitiveEngine(engine).borrow(poolId, owner, delLiquidity, type(uint256).max, data);
IPrimitiveEngine(engine).borrow(poolId, delLiquidity, type(uint256).max, data);
}

function borrowMaxPremium(
bytes32 poolId,
address owner,
uint256 delLiquidity,
uint256 maxPremium,
bytes calldata data
) public {
CALLER = msg.sender;
IPrimitiveEngine(engine).borrow(poolId, delLiquidity, maxPremium, data);
}

function borrowWithoutPaying(
bytes32 poolId,
address owner,
uint256 delLiquidity,
bytes calldata data
) public {
CALLER = msg.sender;
dontPay = 0;
IPrimitiveEngine(engine).borrow(poolId, delLiquidity, type(uint256).max, data);
dontPay = 1;
}

function borrowCallback(
Expand All @@ -39,11 +64,34 @@ contract EngineBorrow {
bytes calldata data
) public {
uint256 riskyNeeded = delLiquidity - delRisky;

if (dontPay == 0) return;
IERC20(risky).transferFrom(CALLER, msg.sender, riskyNeeded);
IERC20(stable).transfer(CALLER, delStable);
}

function repay(
bytes32 poolId,
address owner,
uint256 delLiquidity,
bool fromMargin,
bytes calldata data
)
external
returns (
uint256 delRisky,
uint256 delStable,
uint256 premium
)
{
CALLER = msg.sender;
IPrimitiveEngine(engine).repay(poolId, owner, delLiquidity, fromMargin, data);
}

function repayFromExternalCallback(uint256 delStable, bytes calldata data) external {
IERC20(stable).transferFrom(CALLER, msg.sender, delStable);
IERC20(risky).transfer(CALLER, IERC20(risky).balanceOf(address(this)));
}

function getPosition(bytes32 poolId) public view returns (bytes32 posid) {
posid = keccak256(abi.encodePacked(address(this), poolId));
}
Expand Down
2 changes: 0 additions & 2 deletions contracts/test/engine/EngineCreate.sol
Expand Up @@ -51,8 +51,6 @@ contract EngineCreate {
public
view
returns (
uint128 balanceRisky,
uint128 balanceStable,
uint128 float,
uint128 liquidity,
uint128 debt
Expand Down
8 changes: 3 additions & 5 deletions contracts/test/libraries/TestPosition.sol
Expand Up @@ -28,8 +28,6 @@ contract TestPosition {
function beforeEach(bytes32 poolId, uint256 liquidity) public {
posId = Position.getPositionId(msg.sender, poolId);
positions[posId] = Position.Data({
balanceRisky: 0,
balanceStable: 0,
float: 0,
liquidity: uint128(liquidity), // init with {liquidity} units of liquidity
debt: 0
Expand Down Expand Up @@ -68,13 +66,13 @@ contract TestPosition {
assert(post + uint128(amount) >= pre);
}

/// @notice Increments debt and balanceRisky for a position
/// @notice Increments debt for a position
function shouldBorrow(bytes32 poolId, uint256 amount) public {
Position.Data memory pos = _shouldFetch(msg.sender, poolId);
uint128 pre = pos.balanceRisky;
uint128 pre = pos.debt;
positions.borrow(poolId, amount);
pos = _shouldFetch(msg.sender, poolId);
uint128 post = pos.balanceRisky;
uint128 post = pos.debt;
assert(post >= uint128(amount) + pre);
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -19,11 +19,12 @@
"test:logs": "hardhat test --logs",
"test:engine": "hardhat test ./test/unit/primitiveEngine",
"test:factory": "hardhat test ./test/unit/primitiveFactory",
"test:swap": "hardhat test ./test/unit/primitiveEngine/effect/swap.ts",
"test:create": "hardhat test ./test/unit/primitiveEngine/effect/create.ts",
"test:deposit": "hardhat test ./test/unit/primitiveEngine/effect/deposit.ts",
"test:allocate": "hardhat test ./test/unit/primitiveEngine/effect/allocate.ts",
"test:remove": "hardhat test ./test/unit/primitiveEngine/effect/remove.ts",
"test:swap": "hardhat test ./test/unit/primitiveEngine/effect/swap.ts",
"test:borrow": "hardhat test ./test/unit/primitiveEngine/effect/borrow.ts",
"test:lib": "hardhat test ./test/unit/libraries",
"prepare": "husky install"
},
Expand Down

0 comments on commit 43ef720

Please sign in to comment.