-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
720 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.