Skip to content

Commit

Permalink
feat: contract organization, final cleanup (#14)
Browse files Browse the repository at this point in the history
* feat: contract organization, final cleanup

* refactor: remove unnecessary blank lines

* chore: update scripts to ignore fuzz

* feat: ignore fuzz tests in ci
  • Loading branch information
justingreenberg authored Nov 21, 2023
1 parent b4c06f9 commit 9da4191
Show file tree
Hide file tree
Showing 24 changed files with 439 additions and 129 deletions.
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

0 comments on commit 9da4191

Please sign in to comment.