Skip to content

Commit

Permalink
feat: first working version
Browse files Browse the repository at this point in the history
  • Loading branch information
MathisGD committed Jun 25, 2023
1 parent dde4b1b commit 4339ec0
Show file tree
Hide file tree
Showing 12 changed files with 720 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/solmate"]
path = lib/solmate
url = https://github.com/transmissions11/solmate
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ out = "out"
libs = ["lib"]
via-ir = true

[fmt]
int_types = "short"

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
1 change: 1 addition & 0 deletions lib/solmate
Submodule solmate added at bfc9c2
166 changes: 166 additions & 0 deletions src/Market.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {IERC20} from "src/interfaces/IERC20.sol";
import {IOracle} from "src/interfaces/IOracle.sol";

import {MathLib} from "src/libraries/MathLib.sol";
import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol";

uint constant N = 10;

function bucketToLLTV(uint bucket) pure returns (uint) {
return MathLib.wDiv(bucket, N - 1);
}

function irm(uint utilization) pure returns (uint) {
// Divide by the number of seconds in a year.
// This is a very simple model (to refine later) where x% utilization corresponds to x% APR.
return utilization / 365 days;
}

contract Market {
using MathLib for int;
using MathLib for uint;
using SafeTransferLib for IERC20;

// Constants.

uint public constant getN = N;

address public immutable borrowableAsset;
address public immutable collateralAsset;
address public immutable borrowableOracle;
address public immutable collateralOracle;

// Storage.

// User' supply balances.
mapping(address => mapping(uint => uint)) public supplyShare;
// User' borrow balances.
mapping(address => mapping(uint => uint)) public borrowShare;
// User' collateral balance.
mapping(address => uint) public collateral;
// Market total supply.
mapping(uint => uint) public totalSupply;
// Market total supply shares.
mapping(uint => uint) public totalSupplyShares;
// Market total borrow.
mapping(uint => uint) public totalBorrow;
// Market total borrow shares.
mapping(uint => uint) public totalBorrowShares;
// Interests last update.
uint public lastUpdate;

// Constructor.

constructor(
address newBorrowableAsset,
address newCollateralAsset,
address newBorrowableOracle,
address newCollateralOracle
) {
borrowableAsset = newBorrowableAsset;
collateralAsset = newCollateralAsset;
borrowableOracle = newBorrowableOracle;
collateralOracle = newCollateralOracle;
}

// Suppliers position management.

/// @dev positive amount to deposit.
function modifyDeposit(int amount, uint bucket) external {
if (amount == 0) return;
require(bucket < N, "unknown bucket");

accrueInterests(bucket);

if (totalSupply[bucket] == 0) {
supplyShare[msg.sender][bucket] = 1e18;
totalSupplyShares[bucket] = 1e18;
} else {
int shares = amount.wMul(totalSupplyShares[bucket]).wDiv(totalSupply[bucket]);
supplyShare[msg.sender][bucket] = (int(supplyShare[msg.sender][bucket]) + shares).safeToUint();
totalSupplyShares[bucket] = (int(totalSupplyShares[bucket]) + shares).safeToUint();
}

// No need to check if the integer is positive.
totalSupply[bucket] = uint(int(totalSupply[bucket]) + amount);

if (amount < 0) require(totalBorrow[bucket] <= totalSupply[bucket], "not enough liquidity");

IERC20(borrowableAsset).handleTransfer({user: msg.sender, amountIn: amount});
}

// Borrowers position management.

/// @dev positive amount to borrow (to discuss).
function modifyBorrow(int amount, uint bucket) external {
if (amount == 0) return;
require(bucket < N, "unknown bucket");

accrueInterests(bucket);

if (totalBorrow[bucket] == 0) {
borrowShare[msg.sender][bucket] = 1e18;
totalBorrowShares[bucket] = 1e18;
} else {
int shares = amount.wMul(totalBorrowShares[bucket]).wDiv(totalBorrow[bucket]);
borrowShare[msg.sender][bucket] = (int(borrowShare[msg.sender][bucket]) + shares).safeToUint();
totalBorrowShares[bucket] = (int(totalBorrowShares[bucket]) + shares).safeToUint();
}

// No need to check if the integer is positive.
totalBorrow[bucket] = uint(int(totalBorrow[bucket]) + amount);

if (amount > 0) {
checkHealth(msg.sender);
require(totalBorrow[bucket] <= totalSupply[bucket], "not enough liquidity");
}

IERC20(borrowableAsset).handleTransfer({user: msg.sender, amountIn: -amount});
}

/// @dev positive amount to deposit.
function modifyCollateral(int amount) external {
collateral[msg.sender] = (int(collateral[msg.sender]) + amount).safeToUint();

if (amount < 0) checkHealth(msg.sender);

IERC20(collateralAsset).handleTransfer({user: msg.sender, amountIn: amount});
}

// Interests management.

function accrueInterests(uint bucket) internal {
uint bucketTotalBorrow = totalBorrow[bucket];
uint bucketTotalSupply = totalSupply[bucket];
if (bucketTotalSupply == 0) return;
uint utilization = bucketTotalBorrow.wDiv(bucketTotalSupply);
uint borrowRate = irm(utilization);
uint accruedInterests = bucketTotalBorrow.wMul(borrowRate).wMul(block.timestamp - lastUpdate);

totalSupply[bucket] = bucketTotalSupply + accruedInterests;
totalBorrow[bucket] = bucketTotalBorrow + accruedInterests;
lastUpdate = block.timestamp;
}

// Health check.

function checkHealth(address user) public view {
// Temporary trick to ease testing.
if (IOracle(borrowableOracle).price() == 0) return;
uint collateralValueRequired;
for (uint bucket = 1; bucket < N; bucket++) {
if (totalBorrowShares[bucket] > 0 && borrowShare[user][bucket] > 0) {
uint borrowAtBucket =
borrowShare[user][bucket].wMul(totalBorrow[bucket]).wDiv(totalBorrowShares[bucket]);
collateralValueRequired +=
borrowAtBucket.wMul(IOracle(borrowableOracle).price()).wDiv(bucketToLLTV(bucket));
}
}
require(
collateral[user].wMul(IOracle(collateralOracle).price()) >= collateralValueRequired, "not enough collateral"
);
}
}
7 changes: 7 additions & 0 deletions src/interfaces/IERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IERC20 {
function transferFrom(address, address, uint) external;
function transfer(address, uint) external;
}
6 changes: 6 additions & 0 deletions src/interfaces/IOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IOracle {
function price() external view returns (uint);
}
33 changes: 33 additions & 0 deletions src/libraries/MathLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

/// @notice Maths utils.
library MathLib {
uint internal constant WAD = 1e18;

/// @dev Rounds down.
function wMul(uint x, uint y) internal pure returns (uint z) {
z = x * y / WAD;
}

/// @dev Rounds down.
function wMul(int x, uint y) internal pure returns (int z) {
z = x * int(y) / int(WAD);
}

/// @dev Rounds down.
function wDiv(uint x, uint y) internal pure returns (uint z) {
z = x * WAD / y;
}

/// @dev Rounds down.
function wDiv(int x, uint y) internal pure returns (int z) {
z = x * int(WAD) / int(y);
}

/// @dev Reverts if x is negative.
function safeToUint(int x) internal pure returns (uint z) {
require(x >= 0);
z = uint(x);
}
}
74 changes: 74 additions & 0 deletions src/libraries/SafeTransferLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;

import {IERC20} from "src/interfaces/IERC20.sol";

/// @notice Safe ERC20 transfer library that gracefully handles missing return values.
/// @dev Greatly inspired by Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol).
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
library SafeTransferLib {
function handleTransfer(IERC20 token, address user, int amountIn) internal {
if (amountIn > 0) return safeTransferFrom(token, user, address(this), uint(amountIn));
return safeTransfer(token, user, uint(-amountIn));
}

function safeTransferFrom(IERC20 token, address from, address to, uint amount) private {
bool success;

/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)

// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument.
mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.

success :=
and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 100, 0, 32)
)
}

require(success, "TRANSFER_FROM_FAILED");
}

function safeTransfer(IERC20 token, address to, uint amount) private {
bool success;

/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)

// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.

success :=
and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}

require(success, "TRANSFER_FAILED");
}
}
12 changes: 12 additions & 0 deletions src/mocks/ERC20Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {ERC20} from "solmate/tokens/ERC20.sol";

contract ERC20Mock is ERC20 {
constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20(_name, _symbol, _decimals) {}

function setBalance(address owner, uint amount) external {
balanceOf[owner] = amount;
}
}
12 changes: 12 additions & 0 deletions src/mocks/OracleMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {IOracle} from "src/Market.sol";

contract OracleMock is IOracle {
uint public price;

function setPrice(uint newPrice) external {
price = newPrice;
}
}
Loading

0 comments on commit 4339ec0

Please sign in to comment.