Skip to content

Commit 9c8b076

Browse files
authored
Add impair claim functionality including permit impair (#41)
* Add impair claim functionality including permit impair * fix hh tests * delete all hardhat test related stuff while we are at it, as they are relics of the past and redundant
1 parent 86277a7 commit 9c8b076

47 files changed

Lines changed: 2838 additions & 5136 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/hardhat_test.yml

Lines changed: 0 additions & 24 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,15 @@
1212
},
1313
"scripts": {
1414
"build": "forge build",
15-
"build:hh": "npx hardhat compile",
1615
"test": "forge test --gas-report",
17-
"test:hh": "npx hardhat test",
1816
"dev": "forge test -vvv -w --run-all --gas-report",
1917
"gas": "forge snapshot && forge build --sizes",
2018
"coverage": "forge coverage --report lcov; ex -c 'g/SF:src/mocks/.-1,/end_of_record/d' -c 'wq' lcov.info",
2119
"prepare": "husky install"
2220
},
2321
"devDependencies": {
24-
"@ethersproject/abi": "^5.7.0",
25-
"@ethersproject/providers": "^5.7.1",
26-
"@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
27-
"@nomicfoundation/hardhat-network-helpers": "^1.0.6",
28-
"@nomicfoundation/hardhat-toolbox": "^2.0.0",
29-
"@nomiclabs/hardhat-ethers": "^2.1.1",
30-
"@nomiclabs/hardhat-etherscan": "^3.1.0",
31-
"@typechain/ethers-v5": "^10.1.0",
32-
"@typechain/hardhat": "^6.1.3",
33-
"@types/chai": "^4.3.3",
34-
"chai": "^4.3.6",
35-
"ethers": "^5.7.1",
36-
"hardhat": "^2.11.2",
37-
"hardhat-gas-reporter": "^1.0.9",
38-
"hardhat-preprocessor": "^0.1.5",
3922
"husky": "^7.0.0",
4023
"solidity-coverage": "^0.8.2",
41-
"toml": "^3.0.0",
42-
"ts-node": "^10.9.1",
43-
"typechain": "^8.1.0",
44-
"typescript": "^4.8.3"
24+
"toml": "^3.0.0"
4525
}
4626
}

src/BullaClaim.sol

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
5252
error PastApprovalDeadline();
5353
error NotOwner();
5454
error NotCreditorOrDebtor();
55+
error NotCreditor();
5556
error NotController(address sender);
5657
error ClaimBound();
5758
error ClaimNotPending();
@@ -62,6 +63,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
6263
error ZeroAmount();
6364
error PaymentUnderApproved();
6465
error OverPaying(uint256 paymentAmount);
66+
error StillInGracePeriod();
67+
error NoDueBy();
6568

6669
function _notLocked() internal view {
6770
if (lockState == LockState.Locked) revert Locked();
@@ -93,6 +96,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
9396

9497
event ClaimRescinded(uint256 indexed claimId, address indexed from, string note);
9598

99+
event ClaimImpaired(uint256 indexed claimId);
100+
96101
event CreateClaimApproved(
97102
address indexed user,
98103
address indexed operator,
@@ -113,6 +118,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
113118

114119
event CancelClaimApproved(address indexed user, address indexed operator, uint256 approvalCount);
115120

121+
event ImpairClaimApproved(address indexed user, address indexed operator, uint256 approvalCount);
122+
116123
constructor(address _extensionRegistry, LockState _lockState)
117124
ERC721("BullaClaim", "CLAIM")
118125
EIP712("BullaClaim", "1")
@@ -242,6 +249,10 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
242249
if (params.dueBy != 0 && (params.dueBy < block.timestamp || params.dueBy > type(uint40).max)) {
243250
revert InvalidDueBy();
244251
}
252+
// validate impairment grace period
253+
if (params.impairmentGracePeriod > type(uint40).max) {
254+
revert InvalidDueBy(); // reuse this error for consistency since it's about time validation
255+
}
245256

246257
// you need the permission of the debtor to bind a claim
247258
if (params.binding == ClaimBinding.Bound && from != params.debtor) revert CannotBindClaim();
@@ -266,6 +277,7 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
266277
if (params.binding != ClaimBinding.Unbound) claim.binding = params.binding;
267278
if (params.payerReceivesClaimOnPayment) claim.payerReceivesClaimOnPayment = true;
268279
if (params.dueBy != 0) claim.dueBy = uint40(params.dueBy);
280+
if (params.impairmentGracePeriod != 0) claim.impairmentGracePeriod = uint40(params.impairmentGracePeriod);
269281
}
270282

271283
emit ClaimCreated(
@@ -316,10 +328,10 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
316328
_spendPayClaimApproval(from, msg.sender, claimId, amount);
317329

318330
Claim memory claim = getClaim(claimId);
319-
331+
320332
// Only the controller can call this function
321333
if (claim.controller != msg.sender) revert NotController(msg.sender);
322-
334+
323335
_updateClaimPaymentState(from, claimId, amount);
324336
}
325337

@@ -414,7 +426,7 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
414426

415427
// Update payment state first to follow checks-effects-interactions pattern
416428
_updateClaimPaymentState(from, claimId, paymentAmount);
417-
429+
418430
// Process token transfer after state is updated
419431
claim.token == address(0)
420432
? creditor.safeTransferETH(paymentAmount)
@@ -434,7 +446,9 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
434446
if (paymentAmount == 0) revert PayingZero();
435447

436448
// make sure the claim can be paid (not completed, not rejected, not rescinded)
437-
if (claim.status != Status.Pending && claim.status != Status.Repaying) revert ClaimNotPending();
449+
if (claim.status != Status.Pending && claim.status != Status.Repaying && claim.status != Status.Impaired) {
450+
revert ClaimNotPending();
451+
}
438452

439453
uint256 totalPaidAmount = claim.paidAmount + paymentAmount;
440454
bool claimPaid = totalPaidAmount == claim.claimAmount;
@@ -504,7 +518,9 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
504518
// check if the claim is controlled
505519
if (claim.controller != address(0) && msg.sender != claim.controller) revert NotController(msg.sender);
506520
// make sure the claim is in pending status
507-
if (claim.status != Status.Pending && claim.status != Status.Repaying) revert ClaimNotPending();
521+
if (claim.status != Status.Pending && claim.status != Status.Repaying && claim.status != Status.Impaired) {
522+
revert ClaimNotPending();
523+
}
508524
// make sure the sender is authorized
509525
if (from != creditor && from != claim.debtor) revert NotCreditorOrDebtor();
510526
// make sure the binding is valid
@@ -553,6 +569,21 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
553569
return;
554570
}
555571

572+
/// @notice "spends" an operator's impairClaim approval
573+
/// @notice SPEC:
574+
/// A function can call this function to verify and "spend" `from`'s approval of `operator` to impair a claim given:
575+
/// S1. `operator` has > 0 approvalCount from `from` address -> otherwise: reverts
576+
///
577+
/// RES1: If the above is true, and the approvalCount != type(uint64).max, decrement the approval count by 1 and return
578+
function _spendImpairClaimApproval(address user, address operator) internal {
579+
ImpairClaimApproval storage approval = approvals[user][operator].impairClaim;
580+
581+
if (approval.approvalCount == 0) revert NotApproved();
582+
if (approval.approvalCount != type(uint64).max) approval.approvalCount--;
583+
584+
return;
585+
}
586+
556587
/// @notice allows a creditor to rescind a claim or a debtor to reject a claim
557588
/// @notice SPEC:
558589
/// this function will rescind or reject a claim given:
@@ -581,6 +612,56 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
581612
}
582613
}
583614

615+
/**
616+
* /// IMPAIR CLAIM ///
617+
*/
618+
619+
/// @notice allows a creditor to impair a claim
620+
/// @notice SPEC:
621+
/// 1. call impairClaim on behalf of the msg.sender
622+
function impairClaim(uint256 claimId) external {
623+
_impairClaim(msg.sender, claimId);
624+
}
625+
626+
/// @notice allows an operator to impair a claim on behalf of a creditor
627+
/// @notice SPEC:
628+
/// 1. verify and spend msg.sender's approval to impair claim
629+
/// 2. impair the claim on `from`'s behalf
630+
function impairClaimFrom(address from, uint256 claimId) external {
631+
_spendImpairClaimApproval(from, msg.sender);
632+
633+
_impairClaim(from, claimId);
634+
}
635+
636+
/// @notice allows a creditor to impair a claim
637+
/// @notice SPEC:
638+
/// this function will impair a claim given:
639+
/// 1. The contract is not locked
640+
/// 2. The claim exists and is not burned
641+
/// 3. The caller is the creditor (owner of the claim NFT)
642+
/// 4. The claim is in pending or repaying status
643+
/// 5. If claim has a dueBy date, the grace period must have passed
644+
/// 6. If claim has no dueBy date (dueBy = 0), it cannot be impaired
645+
function _impairClaim(address from, uint256 claimId) internal {
646+
_notLocked();
647+
// load the claim from storage
648+
Claim memory claim = getClaim(claimId);
649+
address creditor = _ownerOf[claimId];
650+
651+
if (claim.controller != address(0) && msg.sender != claim.controller) revert NotController(msg.sender);
652+
// make sure the claim can be impaired (pending or repaying)
653+
if (claim.status != Status.Pending && claim.status != Status.Repaying) revert ClaimNotPending();
654+
// only the creditor can impair a claim
655+
if (from != creditor) revert NotCreditor();
656+
657+
// Grace period validation
658+
if (claim.dueBy == 0) revert NoDueBy();
659+
if (block.timestamp < claim.dueBy + claim.impairmentGracePeriod) revert StillInGracePeriod();
660+
661+
claims[claimId].status = Status.Impaired;
662+
emit ClaimImpaired(claimId);
663+
}
664+
584665
/*///////////////////////////////////////////////////////////////
585666
PERMIT FUNCTIONS
586667
//////////////////////////////////////////////////////////////*/
@@ -649,6 +730,14 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
649730
);
650731
}
651732

733+
/// @notice permits an operator to impair claims on user's behalf
734+
/// @dev see BullaClaimPermitLib.sol for spec
735+
function permitImpairClaim(address user, address operator, uint64 approvalCount, bytes calldata signature) public {
736+
BullaClaimPermitLib.permitImpairClaim(
737+
approvals[user][operator], extensionRegistry, _domainSeparatorV4(), user, operator, approvalCount, signature
738+
);
739+
}
740+
652741
/*///////////////////////////////////////////////////////////////
653742
VIEW / UTILITY FUNCTIONS
654743
//////////////////////////////////////////////////////////////*/
@@ -667,7 +756,8 @@ contract BullaClaim is ERC721, EIP712, Ownable, BoringBatchable {
667756
token: claimStorage.token,
668757
controller: claimStorage.controller,
669758
originalCreditor: claimStorage.originalCreditor,
670-
dueBy: claimStorage.dueBy
759+
dueBy: claimStorage.dueBy,
760+
impairmentGracePeriod: claimStorage.impairmentGracePeriod
671761
});
672762
}
673763

0 commit comments

Comments
 (0)