-
Couldn't load subscription status.
- Fork 0
Seamless Custodian Switch #350
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
Changes from all commits
d9b1a51
e1f4f63
ae89f76
2a714b6
e3bf031
c12b783
0bded4d
0532c73
4219b78
d054416
0255902
eb73825
a64a8f5
ea48011
c2985c6
6aa4305
1683002
1d5158e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { Context } from "../libs/Context.sol"; | |
| import { ICustodyEvents } from "../interfaces/events/ICustodyEvents.sol"; | ||
| import { IFundsEvents } from "../interfaces/events/IFundsEvents.sol"; | ||
| import { FermionFNFTLib } from "../libs/FermionFNFTLib.sol"; | ||
| import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; | ||
|
|
||
| /** | ||
| * @title CustodyFacet | ||
|
|
@@ -258,4 +259,292 @@ contract CustodyFacet is Context, CustodyErrors, Access, CustodyLib, ICustodyEve | |
|
|
||
| return checkoutRequest; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Request a custodian update | ||
| * | ||
| * The new custodian initiates the update process by calling this function. | ||
| * The request is valid for 24 hours. | ||
| * A new request can be made only after 24 hours from the previous request. | ||
| * | ||
| * Emits a CustodianUpdateRequested event | ||
| * | ||
| * Reverts if: | ||
| * - Custody region is paused | ||
| * - Caller is not the new custodian's assistant | ||
| * - Any token in the corresponding offer is checked out | ||
| * - The previous request is too recent | ||
| * - The custodian fee period is 0 | ||
| * | ||
| * @param _offerId The offer ID for which to request the custodian update | ||
| * @param _newCustodianId The ID of the new custodian to take over | ||
| * @param _newCustodianFee The new custodian fee parameters including amount and period | ||
| * @param _newCustodianVaultParameters The new custodian vault parameters including minimum and maximum amounts | ||
| */ | ||
| function requestCustodianUpdate( | ||
| uint256 _offerId, | ||
| uint256 _newCustodianId, | ||
| FermionTypes.CustodianFee calldata _newCustodianFee, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In line with #394 please also add here a check to revert if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in commit |
||
| FermionTypes.CustodianVaultParameters calldata _newCustodianVaultParameters | ||
| ) external notPaused(FermionTypes.PausableRegion.Custody) nonReentrant { | ||
| if (_newCustodianFee.period == 0) { | ||
| revert InvalidCustodianFeePeriod(); | ||
| } | ||
|
|
||
| FermionStorage.OfferLookups storage offerLookups = FermionStorage.protocolLookups().offerLookups[_offerId]; | ||
|
|
||
| // Check if there was a recent request | ||
| if (offerLookups.custodianUpdateRequest.requestTimestamp + 1 days > block.timestamp) { | ||
| revert UpdateRequestTooRecent(_offerId, 1 days); | ||
| } | ||
|
|
||
| uint256 currentCustodianId = FermionStorage.protocolEntities().offer[_offerId].custodianId; | ||
|
|
||
| // Validate caller is the new custodian's assistant | ||
| EntityLib.validateAccountRole( | ||
| _newCustodianId, | ||
| _msgSender(), | ||
| FermionTypes.EntityRole.Custodian, | ||
| FermionTypes.AccountRole.Assistant | ||
| ); | ||
|
|
||
| _createCustodianUpdateRequest( | ||
0xlucian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _offerId, | ||
| _newCustodianFee, | ||
| _newCustodianVaultParameters, | ||
| currentCustodianId, | ||
| _newCustodianId, | ||
| offerLookups | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Emergency update of custodian | ||
| * @dev Allows the current custodian's assistant or seller's assistant to force a custodian update | ||
| * | ||
| * This is an emergency function that bypasses: | ||
| * - The time restriction between updates | ||
| * - The owner acceptance requirement | ||
| * - The token status check (all checked-in tokens should be owned by the same owner) | ||
| * | ||
| * The current custodian is paid for the used period. | ||
| * The vault parameters remain unchanged in emergency updates. | ||
| * | ||
| * Emits CustodianUpdateRequested and CustodianUpdateAccepted events | ||
| * | ||
| * Reverts if: | ||
| * - Custody region is paused | ||
| * - Caller is not the current custodian's assistant (if _isCustodianAssistant is true) | ||
| * - Caller is not the seller's assistant (if _isCustodianAssistant is false) | ||
| * - New custodian ID is invalid | ||
| * - There are not enough funds in any vault to pay the current custodian | ||
| * | ||
| * @param _offerId The offer ID for which to update the custodian | ||
| * @param _newCustodianId The ID of the new custodian | ||
| * @param _isCustodianAssistant If true, validates caller as custodian assistant, otherwise as seller assistant | ||
| */ | ||
| function emergencyCustodianUpdate( | ||
| uint256 _offerId, | ||
| uint256 _newCustodianId, | ||
| bool _isCustodianAssistant | ||
| ) external notPaused(FermionTypes.PausableRegion.Custody) nonReentrant { | ||
| FermionStorage.ProtocolEntities storage pe = FermionStorage.protocolEntities(); | ||
| FermionTypes.Offer storage offer = pe.offer[_offerId]; | ||
| FermionStorage.ProtocolLookups storage pl = FermionStorage.protocolLookups(); | ||
| FermionStorage.OfferLookups storage offerLookups = pl.offerLookups[_offerId]; | ||
|
|
||
| uint256 currentCustodianId = offer.custodianId; | ||
|
|
||
| if (_isCustodianAssistant) { | ||
| EntityLib.validateAccountRole( | ||
| currentCustodianId, | ||
| _msgSender(), | ||
| FermionTypes.EntityRole.Custodian, | ||
| FermionTypes.AccountRole.Assistant | ||
| ); | ||
| } else { | ||
| EntityLib.validateSellerAssistantOrFacilitator(offer.sellerId, offer.facilitatorId); | ||
| } | ||
|
|
||
| EntityLib.validateEntityRole( | ||
| _newCustodianId, | ||
| pe.entityData[_newCustodianId].roles, | ||
| FermionTypes.EntityRole.Custodian | ||
| ); | ||
|
|
||
| _createCustodianUpdateRequest( | ||
| _offerId, | ||
| offer.custodianFee, | ||
| offerLookups.custodianVaultParameters, | ||
| currentCustodianId, | ||
| _newCustodianId, | ||
| offerLookups | ||
| ); | ||
|
|
||
| _processCustodianUpdate(_offerId, offer, offerLookups.custodianUpdateRequest, offerLookups); | ||
| } | ||
0xlucian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * @notice Accept a custodian update request | ||
| * @dev Processes the acceptance of a custodian update request by the token owner | ||
| * | ||
| * The current custodian is paid for the used period. | ||
| * The vault parameters are updated with the new custodian's parameters. | ||
| * All items in the offer are updated and the current custodian is paid for each item. | ||
| * Tokens that are checked out are skipped in ownership validation. | ||
| * | ||
| * Emits a CustodianUpdateAccepted event | ||
| * | ||
| * Reverts if: | ||
| * - Custody region is paused | ||
| * - Caller is not the owner of all in-custody tokens in the offer | ||
| * - The update request status is not Requested | ||
| * - The update request has expired | ||
| * - There are not enough funds in any vault to pay the current custodian | ||
| * | ||
| * @param _offerId The offer ID for which to accept the custodian update | ||
| */ | ||
| function acceptCustodianUpdate( | ||
| uint256 _offerId | ||
| ) external notPaused(FermionTypes.PausableRegion.Custody) nonReentrant { | ||
| FermionStorage.ProtocolLookups storage pl = FermionStorage.protocolLookups(); | ||
| FermionStorage.OfferLookups storage offerLookups = pl.offerLookups[_offerId]; | ||
| FermionTypes.CustodianUpdateRequest storage updateRequest = offerLookups.custodianUpdateRequest; | ||
|
|
||
| // Check request hasn't expired | ||
| if (updateRequest.requestTimestamp + 1 days < block.timestamp) { | ||
| revert UpdateRequestExpired(_offerId); | ||
| } | ||
|
|
||
| address fermionFNFTAddress = offerLookups.fermionFNFTAddress; | ||
| address msgSender = _msgSender(); | ||
| uint256 itemCount = offerLookups.itemQuantity; | ||
| uint256 firstTokenId = offerLookups.firstTokenId; | ||
| bool hasInCustodyToken; | ||
| for (uint256 i; i < itemCount; ++i) { | ||
| uint256 tokenId = firstTokenId + i; | ||
| if (pl.tokenLookups[tokenId].checkoutRequest.status != FermionTypes.CheckoutRequestStatus.CheckedOut) { | ||
| address tokenOwner = IERC721(fermionFNFTAddress).ownerOf(tokenId); | ||
| if (tokenOwner != msgSender) { | ||
| revert NotTokenBuyer(_offerId, tokenOwner, msgSender); | ||
| } | ||
| hasInCustodyToken = true; | ||
| } | ||
| } | ||
| if (!hasInCustodyToken) { | ||
| revert NoTokensInCustody(_offerId); | ||
| } | ||
| _processCustodianUpdate( | ||
| _offerId, | ||
| FermionStorage.protocolEntities().offer[_offerId], | ||
| updateRequest, | ||
| offerLookups | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Creates a custodian update request | ||
| * @dev Internal helper function to create and store a custodian update request | ||
| * | ||
| * @param _offerId The offer ID for which to create the update request | ||
| * @param _custodianFee The new custodian fee parameters | ||
| * @param _custodianVaultParameters The new custodian vault parameters | ||
| * @param _currentCustodianId The ID of the current custodian | ||
| * @param _newCustodianId The ID of the new custodian | ||
| * @param _offerLookups The offer lookups storage pointer | ||
| */ | ||
| function _createCustodianUpdateRequest( | ||
| uint256 _offerId, | ||
| FermionTypes.CustodianFee memory _custodianFee, | ||
| FermionTypes.CustodianVaultParameters memory _custodianVaultParameters, | ||
| uint256 _currentCustodianId, | ||
0xlucian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| uint256 _newCustodianId, | ||
| FermionStorage.OfferLookups storage _offerLookups | ||
| ) internal { | ||
| _offerLookups.custodianUpdateRequest = FermionTypes.CustodianUpdateRequest({ | ||
| newCustodianId: _newCustodianId, | ||
| custodianFee: _custodianFee, | ||
| custodianVaultParameters: _custodianVaultParameters, | ||
| requestTimestamp: block.timestamp | ||
| }); | ||
|
|
||
| emit CustodianUpdateRequested( | ||
| _offerId, | ||
| _currentCustodianId, | ||
| _newCustodianId, | ||
| _custodianFee, | ||
| _custodianVaultParameters | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Process custodian update | ||
| * @dev Internal helper function to process the custodian update for the offer. | ||
| * Calculates and pays the current custodian, updates the vault period for each token. | ||
| * Also updates the vault parameters for the new custodian in the offer. | ||
| * | ||
| * Reverts if: | ||
| * - Any token within the offer is checked out | ||
0xlucian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * - There are not enough funds in the vault to pay the current custodian | ||
| * | ||
| * @param _offerId The offer ID for which to process the update | ||
| * @param _offer The offer storage pointer | ||
| * @param _updateRequest The update request storage pointer | ||
| * @param _offerLookups The offer lookups storage pointer | ||
| */ | ||
| function _processCustodianUpdate( | ||
| uint256 _offerId, | ||
| FermionTypes.Offer storage _offer, | ||
| FermionTypes.CustodianUpdateRequest storage _updateRequest, | ||
| FermionStorage.OfferLookups storage _offerLookups | ||
| ) internal { | ||
| FermionStorage.ProtocolLookups storage pl = FermionStorage.protocolLookups(); | ||
|
|
||
| uint256 currentCustodianId = _offer.custodianId; | ||
| uint256 currentCustodianFee = _offer.custodianFee.amount; | ||
| uint256 currentCustodianPeriod = _offer.custodianFee.period; | ||
|
|
||
| uint256 itemCount = _offerLookups.itemQuantity; | ||
| uint256 firstTokenId = _offerLookups.firstTokenId; | ||
|
|
||
| // payout the current custodian | ||
| for (uint256 i; i < itemCount; ++i) { | ||
0xlucian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| uint256 tokenId = firstTokenId + i; | ||
| FermionStorage.TokenLookups storage tokenLookups = pl.tokenLookups[tokenId]; | ||
0xlucian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (tokenLookups.checkoutRequest.status == FermionTypes.CheckoutRequestStatus.CheckedOut) { | ||
zajck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| continue; | ||
| } | ||
| // Calculate and pay the current custodian | ||
| FermionTypes.CustodianFee storage vault = tokenLookups.vault; | ||
| uint256 custodianPayoff = ((block.timestamp - vault.period) * currentCustodianFee) / currentCustodianPeriod; | ||
|
|
||
| if (custodianPayoff > vault.amount) { | ||
| revert InsufficientVaultBalance(tokenId, custodianPayoff, vault.amount); | ||
| } | ||
|
|
||
| increaseAvailableFunds(currentCustodianId, _offer.exchangeToken, custodianPayoff); | ||
| vault.amount -= custodianPayoff; | ||
|
|
||
| // Reset the vault period | ||
| vault.period = block.timestamp; | ||
| } | ||
|
|
||
| FermionTypes.CustodianVaultParameters memory custodianVaultParameters = _updateRequest.custodianVaultParameters; | ||
| FermionTypes.CustodianFee memory custodianFee = _updateRequest.custodianFee; | ||
| uint256 newCustodianId = _updateRequest.newCustodianId; | ||
|
|
||
| _offer.custodianId = newCustodianId; | ||
| _offer.custodianFee = custodianFee; | ||
| _offerLookups.custodianVaultParameters = custodianVaultParameters; | ||
|
|
||
| delete _offerLookups.custodianUpdateRequest; | ||
|
|
||
| emit CustodianUpdateAccepted( | ||
| _offerId, | ||
| currentCustodianId, | ||
| newCustodianId, | ||
| custodianFee, | ||
| custodianVaultParameters | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.