Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: contract organization, final cleanup #14

Merged
merged 4 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/format_snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
run: forge fmt

- name: Snapshot
run: forge snapshot --diff --no-match-test /^testFuzz/g
run: forge snapshot --diff --no-match-path *fuzz*

- name: Test
run: forge test -vvv
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ jobs:
run: forge test -vvv

- name: Snapshot
run: forge snapshot --diff --no-match-test /^testFuzz/g
run: forge snapshot --diff --no-match-path "*fuzz*"
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
. "$(dirname -- "$0")/_/husky.sh"

forge fmt --check
forge snapshot --diff --no-match-test /^testFuzz/g
forge snapshot --diff --no-match-path "*fuzz*"

git add .gas-snapshot
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 💫 Astaria v1 Core

<!-- TODO -->

### License

[BUSL-1.1](LICENSE) Copyright 2023 Astaria Labs, Inc.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
},
"scripts": {
"prepare": "husky install",
"snapshot": "forge snapshot --diff --no-match-test /^testFuzz/g"
"snapshot": "forge snapshot --diff --no-match-path *fuzz*"
}
}
47 changes: 38 additions & 9 deletions src/enforcers/AstariaV1BorrowerEnforcer.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2023 Astaria Labs
// SPDX-License-Identifier: BUSL-1.1
// █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ ██╗ ██╗ ██╗
// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ ██║ ██║███║
// ███████║███████╗ ██║ ███████║██████╔╝██║███████║ ██║ ██║╚██║
// ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ ╚██╗ ██╔╝ ██║
// ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ ╚████╔╝ ██║
// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝
//
// Astaria v1 Lending
// Built on Starport https://github.com/astariaXYZ/starport
// Designed with love by Astaria Labs, Inc

pragma solidity ^0.8.17;

Expand All @@ -11,9 +20,17 @@ import {AdditionalTransfer} from "starport-core/lib/StarportLib.sol";
import {AstariaV1Lib} from "v1-core/lib/AstariaV1Lib.sol";

contract AstariaV1BorrowerEnforcer is BorrowerEnforcer {
error LoanRateExceedsCurrentRate();
error LoanAmountOutOfBounds();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

error DebtBundlesNotSupported();
error LoanAmountOutOfBounds();
error LoanRateExceedsCurrentRate();

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STRUCTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

struct V1BorrowerDetails {
uint256 startBlock;
Expand All @@ -24,6 +41,20 @@ contract AstariaV1BorrowerEnforcer is BorrowerEnforcer {
BorrowerEnforcer.Details details;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EXTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice Calculates the current maximum valid rate of a caveat
function locateCurrentRate(bytes calldata caveatData) external view returns (uint256 currentRate) {
V1BorrowerDetails memory v1Details = abi.decode(caveatData, (V1BorrowerDetails));
return _locateCurrentRate(v1Details);
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PUBLIC FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice Validates a loan against a caveat, w/ an inclining rate auction, and a min/max amount
/// @dev Bundle support is not implemented, and will revert
/// @dev The rate in pricing is the endRate.
Expand Down Expand Up @@ -71,11 +102,9 @@ contract AstariaV1BorrowerEnforcer is BorrowerEnforcer {
_validate(additionalTransfers, loan, v1Details.details);
}

/// @notice Calculates the current maximum valid rate of a caveat
function locateCurrentRate(bytes calldata caveatData) external view returns (uint256 currentRate) {
V1BorrowerDetails memory v1Details = abi.decode(caveatData, (V1BorrowerDetails));
return _locateCurrentRate(v1Details);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

function _locateCurrentRate(V1BorrowerDetails memory v1Details) internal view returns (uint256 currentRate) {
uint256 endRate = AstariaV1Lib.getBasePricingRate(v1Details.details.loan.terms.pricingData);
Expand Down
31 changes: 28 additions & 3 deletions src/enforcers/AstariaV1LenderEnforcer.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2023 Astaria Labs
// SPDX-License-Identifier: BUSL-1.1
// █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ ██╗ ██╗ ██╗
// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ ██║ ██║███║
// ███████║███████╗ ██║ ███████║██████╔╝██║███████║ ██║ ██║╚██║
// ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ ╚██╗ ██╔╝ ██║
// ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ ╚████╔╝ ██║
// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝
//
// Astaria v1 Lending
// Built on Starport https://github.com/astariaXYZ/starport
// Designed with love by Astaria Labs, Inc

pragma solidity ^0.8.17;

Expand All @@ -13,17 +22,33 @@ import {AstariaV1Lib} from "v1-core/lib/AstariaV1Lib.sol";
import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol";

contract AstariaV1LenderEnforcer is LenderEnforcer {
uint256 constant MAX_DURATION = uint256(3 * 365 * 1 days); // 3 years
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

error LoanAmountExceedsCaveatAmount();
error LoanRateLessThanCaveatRate();
error DebtBundlesNotSupported();

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS AND IMMUTABLES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

uint256 constant MAX_DURATION = uint256(3 * 365 * 1 days); // 3 years

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STRUCTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

struct V1LenderDetails {
bool matchIdentifier;
LenderEnforcer.Details details;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PUBLIC FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice Validates a loan against a caveat, w/ a minimum rate and a maximum amount
/// @dev Bundle support is not implemented, and will revert
/// @dev matchIdentifier = false will allow the loan to have a different identifier than the caveat
Expand Down
75 changes: 50 additions & 25 deletions src/lib/AstariaV1Lib.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2023 Astaria Labs
// SPDX-License-Identifier: BUSL-1.1
// █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ ██╗ ██╗ ██╗
// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ ██║ ██║███║
// ███████║███████╗ ██║ ███████║██████╔╝██║███████║ ██║ ██║╚██║
// ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ ╚██╗ ██╔╝ ██║
// ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ ╚████╔╝ ██║
// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝
//
// Astaria v1 Lending
// Built on Starport https://github.com/astariaXYZ/starport
// Designed with love by Astaria Labs, Inc

pragma solidity ^0.8.17;

Expand All @@ -16,35 +25,67 @@ library AstariaV1Lib {
using FixedPointMathLib for uint256;
using FixedPointMathLib for int256;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS AND IMMUTABLES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

uint256 constant WAD = 18;
uint256 constant MAX_DURATION = uint256(3 * 365 * 1 days); // 3 years

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

error InterestAccrualRoundingMinimum();
error UnsupportedDecimalValue();
error RateExceedMaxRecallRate();

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PUBLIC FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

function calculateCompoundInterest(uint256 delta_t, uint256 amount, uint256 rate, uint256 decimals)
public
pure
returns (uint256)
{
if (decimals < WAD) {
uint256 baseAdjustment = 10 ** (WAD - decimals);
int256 exponent = int256((rate * baseAdjustment * delta_t) / 365 days);
amount *= baseAdjustment;
uint256 result = amount.mulWad(uint256(exponent.expWad())) - amount;
return result /= baseAdjustment;
}
int256 exponent = int256((rate * delta_t) / 365 days);
return amount.mulWad(uint256(exponent.expWad())) - amount;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

function validateCompoundInterest(uint256 amount, uint256 rate, uint256 recallMax, uint256 decimals)
internal
pure
{
// rate should never exceed the recallMax rate
// Rate should never exceed the recallMax rate
if (rate > recallMax) {
revert RateExceedMaxRecallRate();
}

// only decimal values of 1-18 are supported
// Only decimal values of 1-18 are supported
if (decimals > 18 || decimals == 0) {
revert UnsupportedDecimalValue();
}

// check to validate that the MAX_DURATION does not overflow interest calculation
// creates a maximum safe duration for a loan, loans can go beyond MAX_DURATION with undefined behavior
// Check to validate that the MAX_DURATION does not overflow interest calculation
// Creates a maximum safe duration for a loan, loans can go beyond MAX_DURATION with undefined behavior
calculateCompoundInterest(MAX_DURATION, amount, recallMax, decimals);

// calculate interest for 1 second of time
// loan must produce 1 wei of interest per 1 second of time
// Calculate interest for 1 second of time
// Loan must produce 1 wei of interest per 1 second of time
if (calculateCompoundInterest(1, amount, rate, decimals) == 0) {
// interest does not accrue at least 1 wei per second
// Interest does not accrue at least 1 wei per second
revert InterestAccrualRoundingMinimum();
}
}
Expand Down Expand Up @@ -72,20 +113,4 @@ library AstariaV1Lib {
mstore(add(0x20, pricingData), newRate)
}
}

function calculateCompoundInterest(uint256 delta_t, uint256 amount, uint256 rate, uint256 decimals)
public
pure
returns (uint256)
{
if (decimals < WAD) {
uint256 baseAdjustment = 10 ** (WAD - decimals);
int256 exponent = int256((rate * baseAdjustment * delta_t) / 365 days);
amount *= baseAdjustment;
uint256 result = amount.mulWad(uint256(exponent.expWad())) - amount;
return result /= baseAdjustment;
}
int256 exponent = int256((rate * delta_t) / 365 days);
return amount.mulWad(uint256(exponent.expWad())) - amount;
}
}
43 changes: 32 additions & 11 deletions src/pricing/AstariaV1Pricing.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2023 Astaria Labs
// SPDX-License-Identifier: BUSL-1.1
// █████╗ ███████╗████████╗ █████╗ ██████╗ ██╗ █████╗ ██╗ ██╗ ██╗
// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║██╔══██╗ ██║ ██║███║
// ███████║███████╗ ██║ ███████║██████╔╝██║███████║ ██║ ██║╚██║
// ██╔══██║╚════██║ ██║ ██╔══██║██╔══██╗██║██╔══██║ ╚██╗ ██╔╝ ██║
// ██║ ██║███████║ ██║ ██║ ██║██║ ██║██║██║ ██║ ╚████╔╝ ██║
// ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═══╝ ╚═╝
//
// Astaria v1 Lending
// Built on Starport https://github.com/astariaXYZ/starport
// Designed with love by Astaria Labs, Inc

pragma solidity ^0.8.17;

Expand All @@ -21,10 +30,22 @@ contract AstariaV1Pricing is CompoundInterestPricing {
using FixedPointMathLib for uint256;
using {StarportLib.getId} for Starport.Loan;

constructor(Starport SP_) Pricing(SP_) {}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

error InsufficientRefinance();

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTRUCTOR */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

constructor(Starport SP_) Pricing(SP_) {}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EXTERNAL FUNCTIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

// @inheritdoc Pricing
function getRefinanceConsideration(Starport.Loan calldata loan, bytes calldata newPricingData, address fulfiller)
external
Expand All @@ -37,9 +58,9 @@ contract AstariaV1Pricing is CompoundInterestPricing {
AdditionalTransfer[] memory recallConsideration
)
{
// borrowers can refinance a loan at any time
// Borrowers can refinance a loan at any time
if (fulfiller != loan.borrower) {
// check if a recall is occuring
// Check if a recall is occuring
AstariaV1Status status = AstariaV1Status(loan.terms.status);

Details memory newDetails = abi.decode(newPricingData, (Details));
Expand All @@ -48,23 +69,23 @@ contract AstariaV1Pricing is CompoundInterestPricing {
revert InvalidRefinance();
}
uint256 rate = status.getRecallRate(loan);
// offered loan did not meet the terms of the recall auction
// Offered loan did not meet the terms of the recall auction
if (newDetails.rate > rate) {
revert InsufficientRefinance();
}

uint256 proportion;
address payable receiver = payable(loan.issuer);
uint256 loanId = loan.getId();
// scenario where the recaller is not penalized
// recaller stake is refunded
// Scenario where the recaller is not penalized
// Recaller stake is refunded
if (newDetails.rate > oldDetails.rate) {
proportion = 0;
(receiver,) = status.recalls(loanId);
} else {
// scenario where the recaller is penalized
// essentially the old lender and the new lender split the stake of the recaller
// split is proportional to the difference in rate
// Scenario where the recaller is penalized
// Essentially the old lender and the new lender split the stake of the recaller
// Split is proportional to the difference in rate
proportion = (oldDetails.rate - newDetails.rate).divWad(oldDetails.rate);
}
recallConsideration = status.generateRecallConsideration(loan, proportion, fulfiller, receiver);
Expand Down
Loading