From 2c55f66131040e581c67941a66ccca31d14b2527 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 08:57:42 +0200 Subject: [PATCH 01/83] Setup --- src/managers/LoansManager.sol | 85 +++++++++++++++++++++++++++ src/misc/ERC6909.sol | 107 ++++++++++++++++++++++++++++++++++ src/misc/ERC6909NFT.sol | 52 +++++++++++++++++ src/misc/types/D18.sol | 7 ++- 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/managers/LoansManager.sol create mode 100644 src/misc/ERC6909.sol create mode 100644 src/misc/ERC6909NFT.sol diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol new file mode 100644 index 000000000..4ae5b4eaa --- /dev/null +++ b/src/managers/LoansManager.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Auth} from "src/misc/Auth.sol"; +import {IERC6909NFT} from "src/misc/interfaces/IERC6909.sol"; +import {ERC6909NFT} from "src/misc/ERC6909NFT.sol"; +import {D18, d18} from "src/misc/types/D18.sol"; + +import {PoolId} from "src/common/types/PoolId.sol"; +import {ShareClassId} from "src/common/types/ShareClassId.sol"; + +import {IBalanceSheet} from "src/vaults/interfaces/IBalanceSheet.sol"; +import {IPoolManager} from "src/vaults/interfaces/IPoolManager.sol"; + +struct Loan { + ShareClassId scId; + address owner; + address asset; + D18 outstanding; + D18 totalBorrowed; + D18 totalRepaid; +} + +contract LoansManager is ERC6909NFT { + error InvalidLoan(); + error NonZeroOutstanding(); + + PoolId public immutable poolId; + + IPoolManager public poolManager; + IBalanceSheet public balanceSheet; + + mapping (uint256 tokenId => Loan) public loans; + + constructor(PoolId poolId_, IPoolManager poolManager_, IBalanceSheet balanceSheet_, address deployer) ERC6909NFT(deployer) { + poolId = poolId_; + poolManager = poolManager_; + balanceSheet = balanceSheet_; + } + + function create(ShareClassId scId, address owner, address asset, string memory tokenURI) external { + uint256 loanId = mint(address(this), tokenURI); + poolManager.registerAsset(poolId.centrifugeId(), address(this), loanId); + + loans[loanId] = Loan({ + scId: scId, + owner: owner, + asset: asset, + outstanding: d18(0), + totalBorrowed: d18(0), + totalRepaid: d18(0) + }); + + balanceSheet.deposit(poolId, scId, address(this), loanId, address(this), 1); + } + + function borrow(uint256 loanId, uint128 amount, address receiver) external { + Loan storage loan = loans[loanId]; + require(loan.owner != address(0), InvalidLoan()); + + loan.outstanding = loan.outstanding + d18(amount); + loan.totalBorrowed = loan.totalBorrowed + d18(amount); + + balanceSheet.withdraw(poolId, loan.scId, loan.asset, loanId, receiver, amount); + } + + function repay(uint256 loanId, uint128 amount, address owner) external { + Loan storage loan = loans[loanId]; + require(loan.owner != address(0), InvalidLoan()); + + loan.outstanding = loan.outstanding - d18(amount); + loan.totalRepaid = loan.totalRepaid + d18(amount); + + balanceSheet.deposit(poolId, loan.scId, loan.asset, loanId, owner, amount); + } + + function close(uint256 loanId) external { + Loan storage loan = loans[loanId]; + require(loan.owner != address(0), InvalidLoan()); + require(loan.outstanding.isNull(), NonZeroOutstanding()); + + balanceSheet.withdraw(poolId, loan.scId, address(this), loanId, address(this), 1); + _burn(address(this), loanId, 1); + } +} diff --git a/src/misc/ERC6909.sol b/src/misc/ERC6909.sol new file mode 100644 index 000000000..fc7f55423 --- /dev/null +++ b/src/misc/ERC6909.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {IERC165} from "forge-std/interfaces/IERC165.sol"; + +import {IERC6909} from "src/misc/interfaces/IERC6909.sol"; + +/// @title Basic implementation of all properties according to the ERC6909. +/// +/// @dev This implementation MUST be extended with another contract which defines how tokens are created. +/// Either implement mint/burn or override transfer/transferFrom. +abstract contract ERC6909 is IERC6909 { + mapping(address owner => mapping(uint256 tokenId => uint256)) public balanceOf; + mapping(address owner => mapping(address operator => bool)) public isOperator; + mapping(address owner => mapping(address spender => mapping(uint256 tokenId => uint256))) public allowance; + + /// @inheritdoc IERC6909 + function transfer(address receiver, uint256 tokenId, uint256 amount) external virtual returns (bool) { + return _transfer(msg.sender, receiver, tokenId, amount); + } + + /// @inheritdoc IERC6909 + function transferFrom(address sender, address receiver, uint256 tokenId, uint256 amount) + external + virtual + returns (bool) + { + return _transferFrom(msg.sender, sender, receiver, tokenId, amount); + } + + /// @inheritdoc IERC6909 + function approve(address spender, uint256 tokenId, uint256 amount) external returns (bool) { + allowance[msg.sender][spender][tokenId] = amount; + + emit Approval(msg.sender, spender, tokenId, amount); + + return true; + } + + /// @inheritdoc IERC6909 + function setOperator(address operator, bool approved) external returns (bool) { + isOperator[msg.sender][operator] = approved; + + emit OperatorSet(msg.sender, operator, approved); + + return true; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { + return type(IERC6909).interfaceId == interfaceId || type(IERC165).interfaceId == interfaceId; + } + + function _mint(address owner, uint256 tokenId, uint256 amount) internal { + require(owner != address(0), EmptyOwner()); + require(tokenId > 0, InvalidTokenId()); + require(amount > 0, EmptyAmount()); + + balanceOf[owner][tokenId] += amount; + + emit Transfer(msg.sender, address(0), owner, tokenId, amount); + } + + function _burn(address owner, uint256 tokenId, uint256 amount) internal { + uint256 balance = balanceOf[owner][tokenId]; + require(balance >= amount, InsufficientBalance(msg.sender, tokenId)); + + // The underflow check is handled by the require line above + unchecked { + balanceOf[owner][tokenId] -= amount; + } + + emit Transfer(owner, owner, address(0), tokenId, amount); + } + + function _transferFrom(address spender, address sender, address receiver, uint256 tokenId, uint256 amount) + internal + returns (bool) + { + if (spender != sender && !isOperator[sender][spender]) { + uint256 allowed = allowance[sender][spender][tokenId]; + if (allowed != type(uint256).max) { + require(amount <= allowed, InsufficientAllowance(spender, tokenId)); + allowance[sender][spender][tokenId] -= amount; + } + } + + return _transfer(sender, receiver, tokenId, amount); + } + + function _transfer(address sender, address receiver, uint256 tokenId, uint256 amount) internal returns (bool) { + uint256 senderBalance = balanceOf[sender][tokenId]; + require(senderBalance >= amount, InsufficientBalance(sender, tokenId)); + + // The require check few lines above guarantees that + // it cannot underflow. + unchecked { + balanceOf[sender][tokenId] -= amount; + } + + balanceOf[receiver][tokenId] += amount; + + emit Transfer(msg.sender, sender, receiver, tokenId, amount); + + return true; + } +} diff --git a/src/misc/ERC6909NFT.sol b/src/misc/ERC6909NFT.sol new file mode 100644 index 000000000..c5f85965d --- /dev/null +++ b/src/misc/ERC6909NFT.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {ERC6909} from "src/misc/ERC6909.sol"; +import {StringLib} from "src/misc/libraries/StringLib.sol"; +import {Auth} from "src/misc/Auth.sol"; +import {IERC6909NFT, IERC6909URIExt} from "src/misc/interfaces/IERC6909.sol"; + +contract ERC6909NFT is ERC6909, Auth, IERC6909NFT { + using StringLib for string; + + uint8 constant MAX_SUPPLY = 1; + + uint256 public latestTokenId; + + /// @inheritdoc IERC6909URIExt + string public contractURI; + /// @inheritdoc IERC6909URIExt + mapping(uint256 tokenId => string URI) public tokenURI; + + constructor(address _owner) Auth(_owner) {} + + /// @inheritdoc IERC6909NFT + function setTokenURI(uint256 tokenId, string memory URI) public auth { + tokenURI[tokenId] = URI; + + emit TokenURISet(tokenId, URI); + } + + /// @inheritdoc IERC6909NFT + function mint(address owner, string memory tokenURI_) public auth returns (uint256 tokenId) { + require(!tokenURI_.isEmpty(), EmptyURI()); + + tokenId = ++latestTokenId; + + _mint(owner, tokenId, MAX_SUPPLY); + + setTokenURI(tokenId, tokenURI_); + } + + /// @inheritdoc IERC6909NFT + function burn(uint256 tokenId) external { + _burn(msg.sender, tokenId, 1); + } + + // @inheritdoc IERC6909NFT + function setContractURI(string calldata URI) external auth { + contractURI = URI; + + emit ContractURISet(address(this), URI); + } +} diff --git a/src/misc/types/D18.sol b/src/misc/types/D18.sol index f1219d9fb..50842b61a 100644 --- a/src/misc/types/D18.sol +++ b/src/misc/types/D18.sol @@ -92,6 +92,10 @@ function d18(uint128 num, uint128 den) pure returns (D18) { return D18.wrap(MathLib.mulDiv(num, 1e18, den).toUint128()); } +function isNull(D18 d) pure returns (bool) { + return D18.unwrap(d) == 0; +} + function eq(D18 a, D18 b) pure returns (bool) { return D18.unwrap(a) == D18.unwrap(b); } @@ -112,5 +116,6 @@ using { reciprocalMulUint128, reciprocalMulUint256, reciprocal, - raw + raw, + isNull } for D18 global; From 5498cb85621f82742b6ae6ad433adf35c32a0eea Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 09:00:48 +0200 Subject: [PATCH 02/83] Add valuation --- src/managers/LoansManager.sol | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index 4ae5b4eaa..d25cd4bb3 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -5,6 +5,7 @@ import {Auth} from "src/misc/Auth.sol"; import {IERC6909NFT} from "src/misc/interfaces/IERC6909.sol"; import {ERC6909NFT} from "src/misc/ERC6909NFT.sol"; import {D18, d18} from "src/misc/types/D18.sol"; +import {IERC7726} from "src/misc/interfaces/IERC7726.sol"; import {PoolId} from "src/common/types/PoolId.sol"; import {ShareClassId} from "src/common/types/ShareClassId.sol"; @@ -21,7 +22,7 @@ struct Loan { D18 totalRepaid; } -contract LoansManager is ERC6909NFT { +contract LoansManager is ERC6909NFT, IERC7726 { error InvalidLoan(); error NonZeroOutstanding(); @@ -38,6 +39,10 @@ contract LoansManager is ERC6909NFT { balanceSheet = balanceSheet_; } + //---------------------------------------------------------------------------------------------- + // Open/close + //---------------------------------------------------------------------------------------------- + function create(ShareClassId scId, address owner, address asset, string memory tokenURI) external { uint256 loanId = mint(address(this), tokenURI); poolManager.registerAsset(poolId.centrifugeId(), address(this), loanId); @@ -54,6 +59,19 @@ contract LoansManager is ERC6909NFT { balanceSheet.deposit(poolId, scId, address(this), loanId, address(this), 1); } + function close(uint256 loanId) external { + Loan storage loan = loans[loanId]; + require(loan.owner != address(0), InvalidLoan()); + require(loan.outstanding.isNull(), NonZeroOutstanding()); + + balanceSheet.withdraw(poolId, loan.scId, address(this), loanId, address(this), 1); + _burn(address(this), loanId, 1); + } + + //---------------------------------------------------------------------------------------------- + // Ongoing + //---------------------------------------------------------------------------------------------- + function borrow(uint256 loanId, uint128 amount, address receiver) external { Loan storage loan = loans[loanId]; require(loan.owner != address(0), InvalidLoan()); @@ -74,12 +92,13 @@ contract LoansManager is ERC6909NFT { balanceSheet.deposit(poolId, loan.scId, loan.asset, loanId, owner, amount); } - function close(uint256 loanId) external { - Loan storage loan = loans[loanId]; - require(loan.owner != address(0), InvalidLoan()); - require(loan.outstanding.isNull(), NonZeroOutstanding()); - - balanceSheet.withdraw(poolId, loan.scId, address(this), loanId, address(this), 1); - _burn(address(this), loanId, 1); + //---------------------------------------------------------------------------------------------- + // Valuation + //---------------------------------------------------------------------------------------------- + + function getQuote(uint256 baseAmount, address base, address quote) external view returns (uint256 quoteAmount) { + // TODO: calculate valuation of loan using outstanding supply + quoteAmount = 0; } + } From 20deae2ca71c4ea70d44ff864cb8251c6caeda9b Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 09:02:42 +0200 Subject: [PATCH 03/83] Add comment --- src/managers/LoansManager.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index d25cd4bb3..3aa5960fa 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -20,6 +20,7 @@ struct Loan { D18 outstanding; D18 totalBorrowed; D18 totalRepaid; + // TODO: add rate ID, integrate with Linear Accrual contract } contract LoansManager is ERC6909NFT, IERC7726 { From 6052a5e389c58ae020643572fc565a82c5b7589b Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 09:07:00 +0200 Subject: [PATCH 04/83] Add LTV --- src/managers/LoansManager.sol | 14 ++++++++++++-- src/misc/types/D18.sol | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index 3aa5960fa..782f80596 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -14,18 +14,25 @@ import {IBalanceSheet} from "src/vaults/interfaces/IBalanceSheet.sol"; import {IPoolManager} from "src/vaults/interfaces/IPoolManager.sol"; struct Loan { + // Fixed properties ShareClassId scId; address owner; address asset; + D18 ltv; + D18 value; + + // TODO: add rate ID, integrate with Linear Accrual contract + + // Ongoing D18 outstanding; D18 totalBorrowed; D18 totalRepaid; - // TODO: add rate ID, integrate with Linear Accrual contract } contract LoansManager is ERC6909NFT, IERC7726 { error InvalidLoan(); error NonZeroOutstanding(); + error ExceedsLTV(); PoolId public immutable poolId; @@ -44,7 +51,7 @@ contract LoansManager is ERC6909NFT, IERC7726 { // Open/close //---------------------------------------------------------------------------------------------- - function create(ShareClassId scId, address owner, address asset, string memory tokenURI) external { + function create(ShareClassId scId, address owner, address asset, string memory tokenURI, uint128 ltv, uint128 value) external { uint256 loanId = mint(address(this), tokenURI); poolManager.registerAsset(poolId.centrifugeId(), address(this), loanId); @@ -52,6 +59,8 @@ contract LoansManager is ERC6909NFT, IERC7726 { scId: scId, owner: owner, asset: asset, + ltv: d18(ltv), + value: d18(value), outstanding: d18(0), totalBorrowed: d18(0), totalRepaid: d18(0) @@ -76,6 +85,7 @@ contract LoansManager is ERC6909NFT, IERC7726 { function borrow(uint256 loanId, uint128 amount, address receiver) external { Loan storage loan = loans[loanId]; require(loan.owner != address(0), InvalidLoan()); + require(loan.outstanding + d18(amount) <= loan.ltv * loan.value, ExceedsLTV()); loan.outstanding = loan.outstanding + d18(amount); loan.totalBorrowed = loan.totalBorrowed + d18(amount); diff --git a/src/misc/types/D18.sol b/src/misc/types/D18.sol index 50842b61a..9195b6b92 100644 --- a/src/misc/types/D18.sol +++ b/src/misc/types/D18.sol @@ -100,6 +100,10 @@ function eq(D18 a, D18 b) pure returns (bool) { return D18.unwrap(a) == D18.unwrap(b); } +function lte(D18 a, D18 b) pure returns (bool) { + return D18.unwrap(a) <= D18.unwrap(b); +} + function raw(D18 d) pure returns (uint128) { return D18.unwrap(d); } @@ -108,6 +112,7 @@ using { add as +, sub as -, divD18 as /, + lte as <=, inner, eq, mulD18 as *, From 97eadce1ed89bdaa3dfbdadcc962c47fe52d4228 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 09:10:44 +0200 Subject: [PATCH 05/83] Add comment --- src/managers/LoansManager.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index 782f80596..09d0e769a 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -67,6 +67,8 @@ contract LoansManager is ERC6909NFT, IERC7726 { }); balanceSheet.deposit(poolId, scId, address(this), loanId, address(this), 1); + + // TODO: Hub.updateHoldingValuation() } function close(uint256 loanId) external { From af46b67735969755813f93b50d96687e50ffc11a Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 09:11:31 +0200 Subject: [PATCH 06/83] Fix owner check --- src/managers/LoansManager.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index 09d0e769a..dfd793c15 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -30,7 +30,7 @@ struct Loan { } contract LoansManager is ERC6909NFT, IERC7726 { - error InvalidLoan(); + error NotTheOwner(); error NonZeroOutstanding(); error ExceedsLTV(); @@ -73,7 +73,7 @@ contract LoansManager is ERC6909NFT, IERC7726 { function close(uint256 loanId) external { Loan storage loan = loans[loanId]; - require(loan.owner != address(0), InvalidLoan()); + require(loan.owner == msg.sender, NotTheOwner()); require(loan.outstanding.isNull(), NonZeroOutstanding()); balanceSheet.withdraw(poolId, loan.scId, address(this), loanId, address(this), 1); @@ -86,7 +86,7 @@ contract LoansManager is ERC6909NFT, IERC7726 { function borrow(uint256 loanId, uint128 amount, address receiver) external { Loan storage loan = loans[loanId]; - require(loan.owner != address(0), InvalidLoan()); + require(loan.owner == msg.sender, NotTheOwner()); require(loan.outstanding + d18(amount) <= loan.ltv * loan.value, ExceedsLTV()); loan.outstanding = loan.outstanding + d18(amount); @@ -97,7 +97,7 @@ contract LoansManager is ERC6909NFT, IERC7726 { function repay(uint256 loanId, uint128 amount, address owner) external { Loan storage loan = loans[loanId]; - require(loan.owner != address(0), InvalidLoan()); + require(loan.owner == msg.sender, NotTheOwner()); loan.outstanding = loan.outstanding - d18(amount); loan.totalRepaid = loan.totalRepaid + d18(amount); From 5a0da6a47af1fbb2602e57f9e3f4d6b949f5ec4e Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 15:51:03 +0200 Subject: [PATCH 07/83] Add holding creation --- src/managers/LoansManager.sol | 45 ++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index dfd793c15..4de465e31 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -8,8 +8,12 @@ import {D18, d18} from "src/misc/types/D18.sol"; import {IERC7726} from "src/misc/interfaces/IERC7726.sol"; import {PoolId} from "src/common/types/PoolId.sol"; +import {AssetId} from "src/common/types/AssetId.sol"; +import {AccountId} from "src/common/types/AccountId.sol"; import {ShareClassId} from "src/common/types/ShareClassId.sol"; +import {IHub} from "src/hub/interfaces/IHub.sol"; + import {IBalanceSheet} from "src/vaults/interfaces/IBalanceSheet.sol"; import {IPoolManager} from "src/vaults/interfaces/IPoolManager.sol"; @@ -20,7 +24,6 @@ struct Loan { address asset; D18 ltv; D18 value; - // TODO: add rate ID, integrate with Linear Accrual contract // Ongoing @@ -30,19 +33,40 @@ struct Loan { } contract LoansManager is ERC6909NFT, IERC7726 { + error NotHubChain(); error NotTheOwner(); error NonZeroOutstanding(); error ExceedsLTV(); PoolId public immutable poolId; + AccountId public immutable equityAccount; + AccountId public immutable lossAccount; + AccountId public immutable gainAccount; + IHub public hub; IPoolManager public poolManager; IBalanceSheet public balanceSheet; - mapping (uint256 tokenId => Loan) public loans; + mapping(uint256 tokenId => Loan) public loans; + + constructor( + PoolId poolId_, + IHub hub_, + IPoolManager poolManager_, + IBalanceSheet balanceSheet_, + AccountId equityAccount_, + AccountId lossAccount_, + AccountId gainAccount_, + address deployer + ) ERC6909NFT(deployer) { + require(hub_.sender().localCentrifugeId() == poolId_.centrifugeId(), NotHubChain()); - constructor(PoolId poolId_, IPoolManager poolManager_, IBalanceSheet balanceSheet_, address deployer) ERC6909NFT(deployer) { poolId = poolId_; + equityAccount = equityAccount_; + lossAccount = lossAccount_; + gainAccount = gainAccount_; + + hub = hub_; poolManager = poolManager_; balanceSheet = balanceSheet_; } @@ -51,9 +75,11 @@ contract LoansManager is ERC6909NFT, IERC7726 { // Open/close //---------------------------------------------------------------------------------------------- - function create(ShareClassId scId, address owner, address asset, string memory tokenURI, uint128 ltv, uint128 value) external { + function create(ShareClassId scId, address owner, address asset, string memory tokenURI, uint128 ltv, uint128 value) + external + { uint256 loanId = mint(address(this), tokenURI); - poolManager.registerAsset(poolId.centrifugeId(), address(this), loanId); + AssetId assetId = poolManager.registerAsset(poolId.centrifugeId(), address(this), loanId); loans[loanId] = Loan({ scId: scId, @@ -68,7 +94,11 @@ contract LoansManager is ERC6909NFT, IERC7726 { balanceSheet.deposit(poolId, scId, address(this), loanId, address(this), 1); - // TODO: Hub.updateHoldingValuation() + // TODO: how to ensure unique loan ID? + AccountId assetAccount = AccountId.wrap(uint32(loanId << 2)); + hub.createAccount(poolId, assetAccount, true); + + hub.createHolding(poolId, scId, assetId, IERC7726(address(this)), assetAccount, equityAccount, lossAccount, gainAccount); } function close(uint256 loanId) external { @@ -108,10 +138,9 @@ contract LoansManager is ERC6909NFT, IERC7726 { //---------------------------------------------------------------------------------------------- // Valuation //---------------------------------------------------------------------------------------------- - + function getQuote(uint256 baseAmount, address base, address quote) external view returns (uint256 quoteAmount) { // TODO: calculate valuation of loan using outstanding supply quoteAmount = 0; } - } From 4130016e6c21967238c060411f9c21f1f99441d3 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 30 Apr 2025 16:17:01 +0200 Subject: [PATCH 08/83] Format --- src/managers/LoansManager.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index 4de465e31..2ea45e1cd 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -98,7 +98,9 @@ contract LoansManager is ERC6909NFT, IERC7726 { AccountId assetAccount = AccountId.wrap(uint32(loanId << 2)); hub.createAccount(poolId, assetAccount, true); - hub.createHolding(poolId, scId, assetId, IERC7726(address(this)), assetAccount, equityAccount, lossAccount, gainAccount); + hub.createHolding( + poolId, scId, assetId, IERC7726(address(this)), assetAccount, equityAccount, lossAccount, gainAccount + ); } function close(uint256 loanId) external { From 96f991d3707bbe483f2e89b97a8bdbea12d9d67a Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Thu, 1 May 2025 17:04:56 +0200 Subject: [PATCH 09/83] Add linear accrual, externalize 6909 --- src/common/types/AssetId.sol | 4 + src/managers/LoansManager.sol | 108 +++++++++++------- src/misc/ERC6909NFT.sol | 2 +- src/misc/LinearAccrual.sol | 147 +++++++++++++++++++++++++ src/misc/interfaces/ILinearAccrual.sol | 90 +++++++++++++++ src/misc/libraries/Compounding.sol | 34 ++++++ src/misc/libraries/MathLib.sol | 14 +++ 7 files changed, 360 insertions(+), 39 deletions(-) create mode 100644 src/misc/LinearAccrual.sol create mode 100644 src/misc/interfaces/ILinearAccrual.sol create mode 100644 src/misc/libraries/Compounding.sol diff --git a/src/common/types/AssetId.sol b/src/common/types/AssetId.sol index 75ffa7fa3..e9d94c2c3 100644 --- a/src/common/types/AssetId.sol +++ b/src/common/types/AssetId.sol @@ -13,6 +13,10 @@ function addr(AssetId assetId) pure returns (address) { return address(uint160(AssetId.unwrap(assetId))); } +function assetIdFromAddr(address value) pure returns (AssetId assetId) { + return AssetId.wrap(uint128(uint160(value))); +} + function raw(AssetId assetId) pure returns (uint128) { return AssetId.unwrap(assetId); } diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index 2ea45e1cd..e1a5c7389 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -6,9 +6,10 @@ import {IERC6909NFT} from "src/misc/interfaces/IERC6909.sol"; import {ERC6909NFT} from "src/misc/ERC6909NFT.sol"; import {D18, d18} from "src/misc/types/D18.sol"; import {IERC7726} from "src/misc/interfaces/IERC7726.sol"; +import {ILinearAccrual} from "src/misc/interfaces/ILinearAccrual.sol"; import {PoolId} from "src/common/types/PoolId.sol"; -import {AssetId} from "src/common/types/AssetId.sol"; +import {AssetId, assetIdFromAddr} from "src/common/types/AssetId.sol"; import {AccountId} from "src/common/types/AccountId.sol"; import {ShareClassId} from "src/common/types/ShareClassId.sol"; @@ -18,21 +19,26 @@ import {IBalanceSheet} from "src/vaults/interfaces/IBalanceSheet.sol"; import {IPoolManager} from "src/vaults/interfaces/IPoolManager.sol"; struct Loan { - // Fixed properties + // System properties ShareClassId scId; + uint16 tokenId; address owner; - address asset; + // Loan properties + address borrowAsset; + bytes32 rateId; D18 ltv; D18 value; - // TODO: add rate ID, integrate with Linear Accrual contract - - // Ongoing - D18 outstanding; + // Ongoing properties + int128 normalizedDebt; D18 totalBorrowed; D18 totalRepaid; } -contract LoansManager is ERC6909NFT, IERC7726 { +// TODO: maturity date and/or open term + +contract LoansManager is Auth, IERC7726 { + error UnregisteredRateId(); + error TooManyLoans(); error NotHubChain(); error NotTheOwner(); error NonZeroOutstanding(); @@ -43,14 +49,19 @@ contract LoansManager is ERC6909NFT, IERC7726 { AccountId public immutable lossAccount; AccountId public immutable gainAccount; + IERC6909NFT public immutable token; + ILinearAccrual public immutable linearAccrual; + IHub public hub; IPoolManager public poolManager; IBalanceSheet public balanceSheet; - mapping(uint256 tokenId => Loan) public loans; + mapping(AssetId assetId => Loan) public loans; constructor( PoolId poolId_, + IERC6909NFT token_, + ILinearAccrual linearAccrual_, IHub hub_, IPoolManager poolManager_, IBalanceSheet balanceSheet_, @@ -58,7 +69,7 @@ contract LoansManager is ERC6909NFT, IERC7726 { AccountId lossAccount_, AccountId gainAccount_, address deployer - ) ERC6909NFT(deployer) { + ) Auth(deployer) { require(hub_.sender().localCentrifugeId() == poolId_.centrifugeId(), NotHubChain()); poolId = poolId_; @@ -66,6 +77,9 @@ contract LoansManager is ERC6909NFT, IERC7726 { lossAccount = lossAccount_; gainAccount = gainAccount_; + token = token_; + linearAccrual = linearAccrual_; + hub = hub_; poolManager = poolManager_; balanceSheet = balanceSheet_; @@ -75,27 +89,38 @@ contract LoansManager is ERC6909NFT, IERC7726 { // Open/close //---------------------------------------------------------------------------------------------- - function create(ShareClassId scId, address owner, address asset, string memory tokenURI, uint128 ltv, uint128 value) - external - { - uint256 loanId = mint(address(this), tokenURI); - AssetId assetId = poolManager.registerAsset(poolId.centrifugeId(), address(this), loanId); - - loans[loanId] = Loan({ + function create( + ShareClassId scId, + address owner, + address borrowAsset, + bytes32 rateId, + string memory tokenURI, + uint128 ltv, + uint128 value + ) external auth { + require(linearAccrual.rateIdExists(rateId), UnregisteredRateId()); + + uint256 tokenId = token.mint(address(this), tokenURI); + require(tokenId <= type(uint16).max, TooManyLoans()); + AssetId assetId = poolManager.registerAsset(poolId.centrifugeId(), address(this), tokenId); + + loans[assetId] = Loan({ scId: scId, + tokenId: uint16(tokenId), owner: owner, - asset: asset, + borrowAsset: borrowAsset, + rateId: rateId, ltv: d18(ltv), value: d18(value), - outstanding: d18(0), + normalizedDebt: 0, totalBorrowed: d18(0), totalRepaid: d18(0) }); - balanceSheet.deposit(poolId, scId, address(this), loanId, address(this), 1); + balanceSheet.deposit(poolId, scId, address(this), uint16(tokenId), address(this), 1); // TODO: how to ensure unique loan ID? - AccountId assetAccount = AccountId.wrap(uint32(loanId << 2)); + AccountId assetAccount = AccountId.wrap(uint32(uint16(tokenId) << 2)); hub.createAccount(poolId, assetAccount, true); hub.createHolding( @@ -103,46 +128,53 @@ contract LoansManager is ERC6909NFT, IERC7726 { ); } - function close(uint256 loanId) external { - Loan storage loan = loans[loanId]; + function close(AssetId assetId) external { + Loan storage loan = loans[assetId]; require(loan.owner == msg.sender, NotTheOwner()); - require(loan.outstanding.isNull(), NonZeroOutstanding()); + require(loan.normalizedDebt == 0, NonZeroOutstanding()); - balanceSheet.withdraw(poolId, loan.scId, address(this), loanId, address(this), 1); - _burn(address(this), loanId, 1); + balanceSheet.withdraw(poolId, loan.scId, address(this), loan.tokenId, address(this), 1); + token.burn(loan.tokenId); } //---------------------------------------------------------------------------------------------- // Ongoing //---------------------------------------------------------------------------------------------- - function borrow(uint256 loanId, uint128 amount, address receiver) external { - Loan storage loan = loans[loanId]; + function borrow(AssetId assetId, uint128 amount, address receiver) external { + Loan storage loan = loans[assetId]; require(loan.owner == msg.sender, NotTheOwner()); - require(loan.outstanding + d18(amount) <= loan.ltv * loan.value, ExceedsLTV()); + require( + linearAccrual.debt(loan.rateId, loan.normalizedDebt) + int128(amount) + <= int128((loan.ltv * loan.value).inner()), + ExceedsLTV() + ); - loan.outstanding = loan.outstanding + d18(amount); + loan.normalizedDebt = linearAccrual.getModifiedNormalizedDebt(loan.rateId, loan.normalizedDebt, int128(amount)); loan.totalBorrowed = loan.totalBorrowed + d18(amount); - balanceSheet.withdraw(poolId, loan.scId, loan.asset, loanId, receiver, amount); + balanceSheet.withdraw(poolId, loan.scId, loan.borrowAsset, loan.tokenId, receiver, amount); } - function repay(uint256 loanId, uint128 amount, address owner) external { - Loan storage loan = loans[loanId]; + function repay(AssetId assetId, uint128 amount, address owner) external { + Loan storage loan = loans[assetId]; require(loan.owner == msg.sender, NotTheOwner()); - loan.outstanding = loan.outstanding - d18(amount); + loan.normalizedDebt = linearAccrual.getModifiedNormalizedDebt(loan.rateId, loan.normalizedDebt, -int128(amount)); loan.totalRepaid = loan.totalRepaid + d18(amount); - balanceSheet.deposit(poolId, loan.scId, loan.asset, loanId, owner, amount); + balanceSheet.deposit(poolId, loan.scId, loan.borrowAsset, loan.tokenId, owner, amount); } //---------------------------------------------------------------------------------------------- // Valuation //---------------------------------------------------------------------------------------------- - function getQuote(uint256 baseAmount, address base, address quote) external view returns (uint256 quoteAmount) { - // TODO: calculate valuation of loan using outstanding supply - quoteAmount = 0; + function getQuote(uint256, address base, address quote) external view returns (uint256 quoteAmount) { + // TODO: how to know conversion to quote asset? + + Loan storage loan = loans[assetIdFromAddr(base)]; + int128 debt = linearAccrual.debt(loan.rateId, loan.normalizedDebt); + quoteAmount = debt > 0 ? uint256(uint128(debt)) : 0; } } diff --git a/src/misc/ERC6909NFT.sol b/src/misc/ERC6909NFT.sol index c5f85965d..00ff04074 100644 --- a/src/misc/ERC6909NFT.sol +++ b/src/misc/ERC6909NFT.sol @@ -18,7 +18,7 @@ contract ERC6909NFT is ERC6909, Auth, IERC6909NFT { /// @inheritdoc IERC6909URIExt mapping(uint256 tokenId => string URI) public tokenURI; - constructor(address _owner) Auth(_owner) {} + constructor(address deployer) Auth(deployer) {} /// @inheritdoc IERC6909NFT function setTokenURI(uint256 tokenId, string memory URI) public auth { diff --git a/src/misc/LinearAccrual.sol b/src/misc/LinearAccrual.sol new file mode 100644 index 000000000..6a6528d26 --- /dev/null +++ b/src/misc/LinearAccrual.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {ILinearAccrual} from "src/misc/interfaces/ILinearAccrual.sol"; +import {Compounding, CompoundingPeriod} from "src/misc/libraries/Compounding.sol"; +import {MathLib} from "src/misc/libraries/MathLib.sol"; +import {d18, D18, mulUint128} from "src/misc/types/D18.sol"; + +contract LinearAccrual is ILinearAccrual { + using MathLib for uint128; + using MathLib for uint256; + using MathLib for int128; + + mapping(bytes32 rateId => Rate rate) public rates; + mapping(bytes32 rateId => Group group) public groups; + + //---------------------------------------------------------------------------------------------- + // Rate updates + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILinearAccrual + function drip(bytes32 rateId) public { + Rate storage rate = rates[rateId]; + + // Short circuit to save gas + if (rate.lastUpdated == uint64(block.timestamp)) { + return; + } + + Group memory group = groups[rateId]; + + // Determine number of full compounding periods passed since last update + uint64 periodsPassed = Compounding.getPeriodsPassed(group.period, rate.lastUpdated); + + if (periodsPassed > 0) { + rate.accumulatedRate = d18( + rate.accumulatedRate.mulUint128( + uint256(group.ratePerPeriod.inner()).rpow(periodsPassed, 1e18).toUint128(), MathLib.Rounding.Up + ) + ); + + emit RateAccumulated(rateId, rate.accumulatedRate.inner(), periodsPassed); + rate.lastUpdated = uint64(block.timestamp); + } + } + + //---------------------------------------------------------------------------------------------- + // Rate registration + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILinearAccrual + function registerRateId(uint128 ratePerPeriod_, CompoundingPeriod period) public returns (bytes32 rateId) { + D18 ratePerPeriod = d18(ratePerPeriod_); + Group memory group = Group(ratePerPeriod, period); + + rateId = keccak256(abi.encode(group)); + + require(rates[rateId].lastUpdated == 0, RateIdExists(rateId, ratePerPeriod.inner(), period)); + + groups[rateId] = group; + rates[rateId] = Rate(ratePerPeriod, uint64(block.timestamp)); + + emit NewRateId(rateId, ratePerPeriod.inner(), period); + } + + //---------------------------------------------------------------------------------------------- + // View methods + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILinearAccrual + function rateIdExists(bytes32 rateId) public view returns (bool) { + return rates[rateId].lastUpdated > 0; + } + + /// @inheritdoc ILinearAccrual + function getRateId(uint128 rate, CompoundingPeriod period) public pure returns (bytes32) { + Group memory group = Group(d18(rate), period); + + return keccak256(abi.encode(group)); + } + + /// @inheritdoc ILinearAccrual + function getModifiedNormalizedDebt(bytes32 rateId, int128 prevNormalizedDebt, int128 debtChange) + external + view + returns (int128 newNormalizedDebt) + { + _requireNonZeroUpdatedRateId(rateId); + + if (debtChange >= 0) { + return prevNormalizedDebt + + rates[rateId].accumulatedRate.reciprocalMulUint128(uint128(debtChange), MathLib.Rounding.Up).toInt128(); + } else { + return prevNormalizedDebt + - rates[rateId].accumulatedRate.reciprocalMulUint128(uint128(-debtChange), MathLib.Rounding.Up).toInt128(); + } + } + + /// @inheritdoc ILinearAccrual + function getRenormalizedDebt(bytes32 oldRateId, bytes32 newRateId, int128 prevNormalizedDebt) + external + view + returns (int128 newNormalizedDebt) + { + _requireNonZeroUpdatedRateId(newRateId); + + int128 debt_ = debt(oldRateId, prevNormalizedDebt); + + if (debt_ >= 0) { + return rates[newRateId].accumulatedRate.reciprocalMulUint128( + debt_.toUint256().toUint128(), MathLib.Rounding.Up + ).toInt128(); + } else { + return -( + rates[newRateId].accumulatedRate.reciprocalMulUint128( + (-debt_).toUint256().toUint128(), MathLib.Rounding.Up + ).toInt128() + ); + } + } + + /// @inheritdoc ILinearAccrual + function debt(bytes32 rateId, int128 normalizedDebt) public view returns (int128) { + _requireNonZeroUpdatedRateId(rateId); + + // Casting to int128 safe because we don't exceed number of digits of normalizedDebt + // Casting to uint256 necessary for mulDiv + if (normalizedDebt >= 0) { + return normalizedDebt.toUint256().mulDiv(rates[rateId].accumulatedRate.inner(), 1e18).toUint128().toInt128(); + } else { + return -(-normalizedDebt).toUint256().mulDiv(rates[rateId].accumulatedRate.inner(), 1e18).toUint128().toInt128(); + } + } + + //---------------------------------------------------------------------------------------------- + // Internal methods + //---------------------------------------------------------------------------------------------- + + /// @notice Ensures the given rate id was updated in the current block and is not the zero-rate. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// @param rateId Identifier of the rate group + function _requireNonZeroUpdatedRateId(bytes32 rateId) internal view { + require(rates[rateId].lastUpdated != 0 && rates[rateId].accumulatedRate.inner() != 0, RateIdMissing(rateId)); + require(rates[rateId].lastUpdated == block.timestamp, RateIdOutdated(rateId, rates[rateId].lastUpdated)); + } +} diff --git a/src/misc/interfaces/ILinearAccrual.sol b/src/misc/interfaces/ILinearAccrual.sol new file mode 100644 index 000000000..669a26d11 --- /dev/null +++ b/src/misc/interfaces/ILinearAccrual.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {CompoundingPeriod} from "src/misc/libraries/Compounding.sol"; +import {D18} from "src/misc/types/D18.sol"; + +interface ILinearAccrual { + /// @dev Represents the rate accumulator and the timestamp of the last rate update + struct Rate { + /// @dev Accumulated rate index over time + /// @dev Assumes rate to be prefixed by 1, i.e. 5% rate shall be represented as 1.05 = d18(1e18 + 5e16) + D18 accumulatedRate; + /// @dev Timestamp of last rate update + uint64 lastUpdated; + } + + /// @dev Each group corresponds to a particular compound period and the accrual rate per period + struct Group { + /// @dev Rate per compound period + /// @dev Assumes rate to be prefixed by 1, i.e. 5% rate shall be represented as 1.05 = d18(1e18 + 5e16) + D18 ratePerPeriod; + /// @dev Duration of compound period + CompoundingPeriod period; + } + + /// Events + event NewRateId(bytes32 indexed rateId, uint128 indexed ratePerPeriod, CompoundingPeriod period); + event RateAccumulated(bytes32 indexed rateId, uint128 indexed rate, uint64 periodsPassed); + + /// Errors + error RateIdExists(bytes32 rateId, uint128 ratePerPeriod, CompoundingPeriod period); + error RateIdMissing(bytes32 rateId); + error RateIdOutdated(bytes32 rateId, uint64 lastUpdated); + + /// @notice Updates the accumulated rate of the corresponding identifier based on the periods which have passed + /// since the last update + /// @param rateId the id of the interest rate group + function drip(bytes32 rateId) external; + + /// @notice Registers the rate identifier for the given rate and compound period and returns it. + /// @dev Throws if rate has been updated once already implying it has been registered before + /// + /// @param ratePerPeriod Rate per compound period + /// @param period Compounding schedule + function registerRateId(uint128 ratePerPeriod, CompoundingPeriod period) external returns (bytes32 rateId); + + /// @notice Returns whether the rate identifier has been regsitered. + /// + /// @param rateId the id of the interest rate group + function rateIdExists(bytes32 rateId) external view returns (bool); + + /// @notice Returns the rate identifier for the given rate and compound period. + /// + /// @param ratePerPeriod Rate per compound period + /// @param period Compounding schedule + function getRateId(uint128 ratePerPeriod, CompoundingPeriod period) external pure returns (bytes32 rateId); + + /// @notice Returns the sum of the current normalized debt and the normalized change. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// + /// @param rateId Identifier of the rate group + /// @param prevNormalizedDebt Normalized debt before decreasing + /// @param debtChange The amount by which we modify the debt + function getModifiedNormalizedDebt(bytes32 rateId, int128 prevNormalizedDebt, int128 debtChange) + external + view + returns (int128 newNormalizedDebt); + + /// @notice Returns the renormalized debt based on the current rate group after transitioning normalization from + /// the previous one. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// + /// @param oldRateId Identifier of the previous rate group + /// @param newRateId Identifier of the current rate group + /// @param prevNormalizedDebt Normalized debt under previous rate group + function getRenormalizedDebt(bytes32 oldRateId, bytes32 newRateId, int128 prevNormalizedDebt) + external + view + returns (int128 newNormalizedDebt); + + /// @notice Returns the current debt without normalization based on actual block.timestamp (now) and the + /// accumulated rate. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// @param rateId Identifier of the rate group + /// @param normalizedDebt Normalized debt from which we derive the unnormalized debt + function debt(bytes32 rateId, int128 normalizedDebt) external view returns (int128 unnormalizedDebt); +} diff --git a/src/misc/libraries/Compounding.sol b/src/misc/libraries/Compounding.sol new file mode 100644 index 000000000..5e6ee1367 --- /dev/null +++ b/src/misc/libraries/Compounding.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +enum CompoundingPeriod { + Secondly, + Daily +} + +library Compounding { + uint64 constant SECONDS_PER_DAY = 86400; // 60 * 60 * 24 + + /// @notice Returns the amount of seconds for the given compounding period. + /// + /// @dev Default case is `CompoundingPeriod.Daily`. + function getSeconds(CompoundingPeriod period) public pure returns (uint64) { + if (period == CompoundingPeriod.Daily) return SECONDS_PER_DAY; + else return 1; + } + + /// @notice Returns the number of full compounding periods that have passed since a given timestamp. + /// + /// @dev Default case is `CompoundingPeriod.Daily` and returns 0 for any given future timestamp. + function getPeriodsPassed(CompoundingPeriod period, uint64 startTimestamp) public view returns (uint64) { + if (startTimestamp >= block.timestamp) { + return 0; + } else if (period == CompoundingPeriod.Daily) { + uint64 startDay = startTimestamp / SECONDS_PER_DAY; + uint64 nowDay = uint64(block.timestamp) / SECONDS_PER_DAY; + return nowDay - startDay; + } else { + return uint64(block.timestamp) - startTimestamp; + } + } +} diff --git a/src/misc/libraries/MathLib.sol b/src/misc/libraries/MathLib.sol index edc8eb5c1..222edf591 100644 --- a/src/misc/libraries/MathLib.sol +++ b/src/misc/libraries/MathLib.sol @@ -170,12 +170,26 @@ library MathLib { return uint64(value); } + /// @notice Safe type conversion from uint128 to int128 + function toInt128(uint128 _value) internal pure returns (int128) { + require(_value <= uint128(type(int128).max), "MathLib/uint128-to-int128-overflow"); + + return int128(_value); + } /// @notice Safe type conversion from uint256 to uint128. + function toUint128(uint256 value) internal pure returns (uint128) { require(value <= type(uint128).max, Uint128_Overflow()); return uint128(value); } + /// @notice Safe type conversion from int128 to uint128 + function toUint256(int128 _value) internal pure returns (uint256) { + require(_value >= 0, "MathLib/int128-to-uint256-underflow"); + + return uint256(int256(_value)); + } + /// @notice Returns the smallest of two numbers. function min(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? b : a; From 735d2db3aa5ab5312c98f9e33a075384bac16813 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Thu, 1 May 2025 17:08:29 +0200 Subject: [PATCH 10/83] Fix warning --- src/managers/LoansManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index e1a5c7389..af258cd29 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -170,7 +170,7 @@ contract LoansManager is Auth, IERC7726 { // Valuation //---------------------------------------------------------------------------------------------- - function getQuote(uint256, address base, address quote) external view returns (uint256 quoteAmount) { + function getQuote(uint256, address base, address /* quote */) external view returns (uint256 quoteAmount) { // TODO: how to know conversion to quote asset? Loan storage loan = loans[assetIdFromAddr(base)]; From 41d2b317799855e3e5ca7aaf0cb3ed17cccde3b1 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Thu, 1 May 2025 17:33:16 +0200 Subject: [PATCH 11/83] Change auth --- src/managers/LoansManager.sol | 51 +++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index af258cd29..b34a0a4d9 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -22,12 +22,11 @@ struct Loan { // System properties ShareClassId scId; uint16 tokenId; - address owner; + address borrower; // Loan properties address borrowAsset; bytes32 rateId; - D18 ltv; - D18 value; + D18 maxBorrowAmount; // Ongoing properties int128 normalizedDebt; D18 totalBorrowed; @@ -86,17 +85,16 @@ contract LoansManager is Auth, IERC7726 { } //---------------------------------------------------------------------------------------------- - // Open/close + // Owner actions //---------------------------------------------------------------------------------------------- function create( ShareClassId scId, - address owner, + address borrower, address borrowAsset, bytes32 rateId, string memory tokenURI, - uint128 ltv, - uint128 value + uint128 maxBorrowAmount ) external auth { require(linearAccrual.rateIdExists(rateId), UnregisteredRateId()); @@ -107,11 +105,10 @@ contract LoansManager is Auth, IERC7726 { loans[assetId] = Loan({ scId: scId, tokenId: uint16(tokenId), - owner: owner, + borrower: borrower, borrowAsset: borrowAsset, rateId: rateId, - ltv: d18(ltv), - value: d18(value), + maxBorrowAmount: d18(maxBorrowAmount), normalizedDebt: 0, totalBorrowed: d18(0), totalRepaid: d18(0) @@ -128,25 +125,30 @@ contract LoansManager is Auth, IERC7726 { ); } - function close(AssetId assetId) external { + function updateRate(AssetId assetId, bytes32 newRateId) external auth { Loan storage loan = loans[assetId]; - require(loan.owner == msg.sender, NotTheOwner()); - require(loan.normalizedDebt == 0, NonZeroOutstanding()); - balanceSheet.withdraw(poolId, loan.scId, address(this), loan.tokenId, address(this), 1); - token.burn(loan.tokenId); + loan.normalizedDebt = linearAccrual.getRenormalizedDebt(loan.rateId, newRateId, loan.normalizedDebt); + loan.rateId = newRateId; + } + + function updateMaxBorrowAmount(AssetId assetId, uint128 maxBorrowAmount) external auth { + Loan storage loan = loans[assetId]; + require(linearAccrual.debt(loan.rateId, loan.normalizedDebt) <= int128(maxBorrowAmount), ExceedsLTV()); + + loan.maxBorrowAmount = d18(maxBorrowAmount); } //---------------------------------------------------------------------------------------------- - // Ongoing + // Borrower actions //---------------------------------------------------------------------------------------------- function borrow(AssetId assetId, uint128 amount, address receiver) external { Loan storage loan = loans[assetId]; - require(loan.owner == msg.sender, NotTheOwner()); + require(loan.borrower == msg.sender, NotTheOwner()); require( linearAccrual.debt(loan.rateId, loan.normalizedDebt) + int128(amount) - <= int128((loan.ltv * loan.value).inner()), + <= int128(loan.maxBorrowAmount.inner()), ExceedsLTV() ); @@ -158,7 +160,7 @@ contract LoansManager is Auth, IERC7726 { function repay(AssetId assetId, uint128 amount, address owner) external { Loan storage loan = loans[assetId]; - require(loan.owner == msg.sender, NotTheOwner()); + require(loan.borrower == msg.sender, NotTheOwner()); loan.normalizedDebt = linearAccrual.getModifiedNormalizedDebt(loan.rateId, loan.normalizedDebt, -int128(amount)); loan.totalRepaid = loan.totalRepaid + d18(amount); @@ -166,11 +168,20 @@ contract LoansManager is Auth, IERC7726 { balanceSheet.deposit(poolId, loan.scId, loan.borrowAsset, loan.tokenId, owner, amount); } + function close(AssetId assetId) external { + Loan storage loan = loans[assetId]; + require(loan.borrower == msg.sender, NotTheOwner()); + require(loan.normalizedDebt == 0, NonZeroOutstanding()); + + balanceSheet.withdraw(poolId, loan.scId, address(this), loan.tokenId, address(this), 1); + token.burn(loan.tokenId); + } + //---------------------------------------------------------------------------------------------- // Valuation //---------------------------------------------------------------------------------------------- - function getQuote(uint256, address base, address /* quote */) external view returns (uint256 quoteAmount) { + function getQuote(uint256, address base, address /* quote */ ) external view returns (uint256 quoteAmount) { // TODO: how to know conversion to quote asset? Loan storage loan = loans[assetIdFromAddr(base)]; From 4b7d0c30b87713b59a6f69798484cb3af71a9308 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Fri, 2 May 2025 12:13:57 +0200 Subject: [PATCH 12/83] Add update contract methods --- src/common/libraries/MessageLib.sol | 50 +++++++++++++++++++- src/hub/Accounting.sol | 4 +- src/managers/LoansManager.sol | 72 ++++++++++++++--------------- 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/src/common/libraries/MessageLib.sol b/src/common/libraries/MessageLib.sol index 0bf110f4e..59d4d0fd1 100644 --- a/src/common/libraries/MessageLib.sol +++ b/src/common/libraries/MessageLib.sol @@ -66,7 +66,9 @@ enum UpdateContractType { MaxAssetPriceAge, MaxSharePriceAge, Valuation, - SyncDepositMaxReserve + SyncDepositMaxReserve, + LoanMaxBorrowAmount, + LoanRate } /// @dev Used internally in the VaultUpdateMessage (not represent a submessage) @@ -733,6 +735,52 @@ library MessageLib { return abi.encodePacked(UpdateContractType.SyncDepositMaxReserve, t.assetId, t.maxReserve); } + //--------------------------------------- + // UpdateContract.LoanMaxBorrowAmount (submsg) + //--------------------------------------- + + struct UpdateContractLoanMaxBorrowAmount { + uint128 assetId; + uint128 maxBorrowAmount; + } + + function deserializeUpdateContractLoanMaxBorrowAmount(bytes memory data) + internal + pure + returns (UpdateContractLoanMaxBorrowAmount memory) + { + require(updateContractType(data) == UpdateContractType.LoanMaxBorrowAmount, UnknownMessageType()); + + return UpdateContractLoanMaxBorrowAmount({assetId: data.toUint128(1), maxBorrowAmount: data.toUint128(17)}); + } + + function serialize(UpdateContractLoanMaxBorrowAmount memory t) internal pure returns (bytes memory) { + return abi.encodePacked(UpdateContractType.LoanMaxBorrowAmount, t.assetId, t.maxBorrowAmount); + } + + //--------------------------------------- + // UpdateContract.LoanRate (submsg) + //--------------------------------------- + + struct UpdateContractLoanRate { + uint128 assetId; + bytes32 rateId; + } + + function deserializeUpdateContractLoanRate(bytes memory data) + internal + pure + returns (UpdateContractLoanRate memory) + { + require(updateContractType(data) == UpdateContractType.LoanRate, UnknownMessageType()); + + return UpdateContractLoanRate({assetId: data.toUint128(1), rateId: data.toBytes32(17)}); + } + + function serialize(UpdateContractLoanRate memory t) internal pure returns (bytes memory) { + return abi.encodePacked(UpdateContractType.LoanRate, t.assetId, t.rateId); + } + //--------------------------------------- // DepositRequest //--------------------------------------- diff --git a/src/hub/Accounting.sol b/src/hub/Accounting.sol index cdcd271f3..af204dece 100644 --- a/src/hub/Accounting.sol +++ b/src/hub/Accounting.sol @@ -9,8 +9,8 @@ import {IAccounting, JournalEntry} from "src/hub/interfaces/IAccounting.sol"; import {TransientStorageLib} from "src/misc/libraries/TransientStorageLib.sol"; /// @notice In a transaction there can be multiple journal entries for different pools, -/// which can be interleaved. We want entries for the same pool to share the same journal ID. -/// So we're keeping a journal ID per pool in transient storage. +/// which can be interleaved. We want entries for the same pool to share the same journal ID. +/// So we're keeping a journal ID per pool in transient storage. library TransientJournal { function journalId(PoolId poolId) internal view returns (uint256) { return TransientStorageLib.tloadUint256(keccak256(abi.encode("journalId", poolId))); diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index b34a0a4d9..154b57b7d 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -8,13 +8,12 @@ import {D18, d18} from "src/misc/types/D18.sol"; import {IERC7726} from "src/misc/interfaces/IERC7726.sol"; import {ILinearAccrual} from "src/misc/interfaces/ILinearAccrual.sol"; +import {MessageLib, UpdateContractType} from "src/common/libraries/MessageLib.sol"; import {PoolId} from "src/common/types/PoolId.sol"; import {AssetId, assetIdFromAddr} from "src/common/types/AssetId.sol"; import {AccountId} from "src/common/types/AccountId.sol"; import {ShareClassId} from "src/common/types/ShareClassId.sol"; -import {IHub} from "src/hub/interfaces/IHub.sol"; - import {IBalanceSheet} from "src/vaults/interfaces/IBalanceSheet.sol"; import {IPoolManager} from "src/vaults/interfaces/IPoolManager.sol"; @@ -36,9 +35,9 @@ struct Loan { // TODO: maturity date and/or open term contract LoansManager is Auth, IERC7726 { + error UnknownUpdateContractType(); error UnregisteredRateId(); error TooManyLoans(); - error NotHubChain(); error NotTheOwner(); error NonZeroOutstanding(); error ExceedsLTV(); @@ -51,7 +50,6 @@ contract LoansManager is Auth, IERC7726 { IERC6909NFT public immutable token; ILinearAccrual public immutable linearAccrual; - IHub public hub; IPoolManager public poolManager; IBalanceSheet public balanceSheet; @@ -61,7 +59,6 @@ contract LoansManager is Auth, IERC7726 { PoolId poolId_, IERC6909NFT token_, ILinearAccrual linearAccrual_, - IHub hub_, IPoolManager poolManager_, IBalanceSheet balanceSheet_, AccountId equityAccount_, @@ -69,8 +66,6 @@ contract LoansManager is Auth, IERC7726 { AccountId gainAccount_, address deployer ) Auth(deployer) { - require(hub_.sender().localCentrifugeId() == poolId_.centrifugeId(), NotHubChain()); - poolId = poolId_; equityAccount = equityAccount_; lossAccount = lossAccount_; @@ -79,7 +74,6 @@ contract LoansManager is Auth, IERC7726 { token = token_; linearAccrual = linearAccrual_; - hub = hub_; poolManager = poolManager_; balanceSheet = balanceSheet_; } @@ -88,14 +82,42 @@ contract LoansManager is Auth, IERC7726 { // Owner actions //---------------------------------------------------------------------------------------------- + function update(PoolId /* poolId */, ShareClassId, /* scId */ bytes calldata payload) external auth { + uint8 kind = uint8(MessageLib.updateContractType(payload)); + + if (kind == uint8(UpdateContractType.LoanMaxBorrowAmount)) { + MessageLib.UpdateContractLoanMaxBorrowAmount memory m = MessageLib.deserializeUpdateContractLoanMaxBorrowAmount(payload); + + Loan storage loan = loans[AssetId.wrap(m.assetId)]; + require(linearAccrual.debt(loan.rateId, loan.normalizedDebt) <= int128(m.maxBorrowAmount), ExceedsLTV()); + + loan.maxBorrowAmount = d18(m.maxBorrowAmount); + // emit UpdateLoanMaxBorrowAmount(); + } else if (kind == uint8(UpdateContractType.LoanRate)) { + + MessageLib.UpdateContractLoanRate memory m = MessageLib.deserializeUpdateContractLoanRate(payload); + + Loan storage loan = loans[AssetId.wrap(m.assetId)]; + + loan.normalizedDebt = linearAccrual.getRenormalizedDebt(loan.rateId, m.rateId, loan.normalizedDebt); + loan.rateId = m.rateId; + // emit UpdateLoanRate(); + } else { + revert UnknownUpdateContractType(); + } + } + + //---------------------------------------------------------------------------------------------- + // Borrower actions + //---------------------------------------------------------------------------------------------- + function create( ShareClassId scId, address borrower, address borrowAsset, bytes32 rateId, - string memory tokenURI, - uint128 maxBorrowAmount - ) external auth { + string memory tokenURI + ) external { require(linearAccrual.rateIdExists(rateId), UnregisteredRateId()); uint256 tokenId = token.mint(address(this), tokenURI); @@ -108,7 +130,7 @@ contract LoansManager is Auth, IERC7726 { borrower: borrower, borrowAsset: borrowAsset, rateId: rateId, - maxBorrowAmount: d18(maxBorrowAmount), + maxBorrowAmount: d18(0), normalizedDebt: 0, totalBorrowed: d18(0), totalRepaid: d18(0) @@ -116,33 +138,9 @@ contract LoansManager is Auth, IERC7726 { balanceSheet.deposit(poolId, scId, address(this), uint16(tokenId), address(this), 1); - // TODO: how to ensure unique loan ID? - AccountId assetAccount = AccountId.wrap(uint32(uint16(tokenId) << 2)); - hub.createAccount(poolId, assetAccount, true); - - hub.createHolding( - poolId, scId, assetId, IERC7726(address(this)), assetAccount, equityAccount, lossAccount, gainAccount - ); - } - - function updateRate(AssetId assetId, bytes32 newRateId) external auth { - Loan storage loan = loans[assetId]; - - loan.normalizedDebt = linearAccrual.getRenormalizedDebt(loan.rateId, newRateId, loan.normalizedDebt); - loan.rateId = newRateId; - } - - function updateMaxBorrowAmount(AssetId assetId, uint128 maxBorrowAmount) external auth { - Loan storage loan = loans[assetId]; - require(linearAccrual.debt(loan.rateId, loan.normalizedDebt) <= int128(maxBorrowAmount), ExceedsLTV()); - - loan.maxBorrowAmount = d18(maxBorrowAmount); + // emit NewLoan(..); } - //---------------------------------------------------------------------------------------------- - // Borrower actions - //---------------------------------------------------------------------------------------------- - function borrow(AssetId assetId, uint128 amount, address receiver) external { Loan storage loan = loans[assetId]; require(loan.borrower == msg.sender, NotTheOwner()); From 43f336a18a7b82a8a7fd20484d35791cf2c55078 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Mon, 5 May 2025 10:39:36 +0200 Subject: [PATCH 13/83] Setup NAVManager --- src/common/types/AccountId.sol | 6 ++- src/managers/NAVManager.sol | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/managers/NAVManager.sol diff --git a/src/common/types/AccountId.sol b/src/common/types/AccountId.sol index 46ca7da4c..4afe1f21a 100644 --- a/src/common/types/AccountId.sol +++ b/src/common/types/AccountId.sol @@ -7,4 +7,8 @@ function raw(AccountId accountId_) pure returns (uint32) { return AccountId.unwrap(accountId_); } -using {raw} for AccountId global; +function increment(AccountId accountId_) pure returns (AccountId) { + return AccountId.wrap(AccountId.unwrap(accountId_) + 1); +} + +using {raw, increment} for AccountId global; diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol new file mode 100644 index 000000000..83f4e3d3d --- /dev/null +++ b/src/managers/NAVManager.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Auth} from "src/misc/Auth.sol"; +import {D18, d18} from "src/misc/types/D18.sol"; +import {IERC7726} from "src/misc/interfaces/IERC7726.sol"; + +import {IHub} from "src/hub/interfaces/IHub.sol"; +import {PoolId} from "src/common/types/PoolId.sol"; +import {AssetId} from "src/common/types/AssetId.sol"; +import {AccountId} from "src/common/types/AccountId.sol"; +import {ShareClassId} from "src/common/types/ShareClassId.sol"; +import {IAccounting} from "src/hub/interfaces/IAccounting.sol"; +import {IShareClassManager} from "src/hub/interfaces/IShareClassManager.sol"; + +contract NAVManager is Auth { + error InvalidShareClassCount(); + + PoolId public immutable poolId; + ShareClassId public immutable scId; + + AccountId public immutable equityAccount; + AccountId public immutable liabilityAccount; + AccountId public immutable gainAccount; + AccountId public immutable lossAccount; + + IHub public immutable hub; + IAccounting public immutable accounting; + IShareClassManager public immutable shareClassManager; + + AccountId internal assetAccount; + AccountId internal expenseAccount; + + constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { + require(hub.shareClassManager().shareClassCount(poolId_) == 1, InvalidShareClassCount()); + + poolId = poolId_; + scId = scId_; + hub = hub_; + accounting = hub.accounting(); + shareClassManager = hub.shareClassManager(); + + equityAccount = AccountId.wrap(1); + liabilityAccount = AccountId.wrap(2); + gainAccount = AccountId.wrap(3); + lossAccount = AccountId.wrap(4); + hub.createAccount(poolId, equityAccount, false); + hub.createAccount(poolId, liabilityAccount, false); + hub.createAccount(poolId, gainAccount, false); + hub.createAccount(poolId, lossAccount, false); + + assetAccount = AccountId.wrap(5); + } + + //---------------------------------------------------------------------------------------------- + // Account creation + //---------------------------------------------------------------------------------------------- + + function createHolding(AssetId assetId, IERC7726 valuation) external auth { + hub.createAccount(poolId, assetAccount, true); + hub.createHolding(poolId, scId, assetId, valuation, assetAccount, equityAccount, gainAccount, lossAccount); + assetAccount = assetAccount.increment(); + } + + function createLiability(AssetId assetId, IERC7726 valuation) external auth { + hub.createAccount(poolId, expenseAccount, true); + hub.createLiability(poolId, scId, assetId, valuation, expenseAccount, liabilityAccount); + expenseAccount = expenseAccount.increment(); + } + + //---------------------------------------------------------------------------------------------- + // Price updates + //---------------------------------------------------------------------------------------------- + + function updatePricePerShare() external { + (D18 current, D18 stored) = navPoolPerShare(); + hub.updatePricePerShare(poolId, scId, current); + } + + //---------------------------------------------------------------------------------------------- + // Calculations + //---------------------------------------------------------------------------------------------- + + /// @dev NAV = equity + gain - loss - liability + function netAssetValue() public view returns (D18) { + return d18(accounting.accountValue(poolId, equityAccount)) + d18(accounting.accountValue(poolId, gainAccount)) + - d18(accounting.accountValue(poolId, lossAccount)) - d18(accounting.accountValue(poolId, liabilityAccount)); + } + + /// @dev Price = NAV / share class issuance + function navPoolPerShare() public view returns (D18 current, D18 stored) { + D18 nav = netAssetValue(); + (uint128 issuance, D18 prev) = shareClassManager.metrics(scId); + + current = netAssetValue / d18(issuance); + stored = prev; + } +} From 32266dab5e70e83399baf152d846acb3d5713be2 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Mon, 5 May 2025 10:44:37 +0200 Subject: [PATCH 14/83] Fix --- src/hub/interfaces/IAccounting.sol | 2 +- src/managers/LoansManager.sol | 16 ++++++---------- src/managers/NAVManager.sol | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/hub/interfaces/IAccounting.sol b/src/hub/interfaces/IAccounting.sol index a741bc42e..70ec6512a 100644 --- a/src/hub/interfaces/IAccounting.sol +++ b/src/hub/interfaces/IAccounting.sol @@ -85,7 +85,7 @@ interface IAccounting { /// @param account The account to get the value of /// @return isPositive Indicates whether value is a positive or negative value /// @return value The value of the account - function accountValue(PoolId poolId, AccountId account) external returns (bool isPositive, uint128 value); + function accountValue(PoolId poolId, AccountId account) external view returns (bool isPositive, uint128 value); /// @notice Returns whether an account exists /// @param poolId The pool the account belongs to diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol index 154b57b7d..edd59390a 100644 --- a/src/managers/LoansManager.sol +++ b/src/managers/LoansManager.sol @@ -82,11 +82,12 @@ contract LoansManager is Auth, IERC7726 { // Owner actions //---------------------------------------------------------------------------------------------- - function update(PoolId /* poolId */, ShareClassId, /* scId */ bytes calldata payload) external auth { + function update(PoolId, /* poolId */ ShareClassId, /* scId */ bytes calldata payload) external auth { uint8 kind = uint8(MessageLib.updateContractType(payload)); if (kind == uint8(UpdateContractType.LoanMaxBorrowAmount)) { - MessageLib.UpdateContractLoanMaxBorrowAmount memory m = MessageLib.deserializeUpdateContractLoanMaxBorrowAmount(payload); + MessageLib.UpdateContractLoanMaxBorrowAmount memory m = + MessageLib.deserializeUpdateContractLoanMaxBorrowAmount(payload); Loan storage loan = loans[AssetId.wrap(m.assetId)]; require(linearAccrual.debt(loan.rateId, loan.normalizedDebt) <= int128(m.maxBorrowAmount), ExceedsLTV()); @@ -94,7 +95,6 @@ contract LoansManager is Auth, IERC7726 { loan.maxBorrowAmount = d18(m.maxBorrowAmount); // emit UpdateLoanMaxBorrowAmount(); } else if (kind == uint8(UpdateContractType.LoanRate)) { - MessageLib.UpdateContractLoanRate memory m = MessageLib.deserializeUpdateContractLoanRate(payload); Loan storage loan = loans[AssetId.wrap(m.assetId)]; @@ -111,13 +111,9 @@ contract LoansManager is Auth, IERC7726 { // Borrower actions //---------------------------------------------------------------------------------------------- - function create( - ShareClassId scId, - address borrower, - address borrowAsset, - bytes32 rateId, - string memory tokenURI - ) external { + function create(ShareClassId scId, address borrower, address borrowAsset, bytes32 rateId, string memory tokenURI) + external + { require(linearAccrual.rateIdExists(rateId), UnregisteredRateId()); uint256 tokenId = token.mint(address(this), tokenURI); diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 83f4e3d3d..79de1c09f 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -72,8 +72,12 @@ contract NAVManager is Auth { // Price updates //---------------------------------------------------------------------------------------------- + function updateHoldingValue(AssetId assetId) external { + hub.updateHoldingValue(poolId, scId, assetId); + } + function updatePricePerShare() external { - (D18 current, D18 stored) = navPoolPerShare(); + (D18 current,) = navPoolPerShare(); hub.updatePricePerShare(poolId, scId, current); } @@ -83,8 +87,12 @@ contract NAVManager is Auth { /// @dev NAV = equity + gain - loss - liability function netAssetValue() public view returns (D18) { - return d18(accounting.accountValue(poolId, equityAccount)) + d18(accounting.accountValue(poolId, gainAccount)) - - d18(accounting.accountValue(poolId, lossAccount)) - d18(accounting.accountValue(poolId, liabilityAccount)); + // TODO: how to handle when one of the accounts is not positive + (, uint128 equity) = accounting.accountValue(poolId, equityAccount); + (, uint128 gain) = accounting.accountValue(poolId, gainAccount); + (, uint128 loss) = accounting.accountValue(poolId, lossAccount); + (, uint128 liability) = accounting.accountValue(poolId, liabilityAccount); + return d18(equity) + d18(gain) - d18(loss) - d18(liability); } /// @dev Price = NAV / share class issuance @@ -92,7 +100,7 @@ contract NAVManager is Auth { D18 nav = netAssetValue(); (uint128 issuance, D18 prev) = shareClassManager.metrics(scId); - current = netAssetValue / d18(issuance); + current = nav / d18(issuance); stored = prev; } } From acc84f0cecf1440aace7d6aad3c25c6187eaf81d Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Mon, 5 May 2025 10:47:03 +0200 Subject: [PATCH 15/83] Fix --- src/managers/NAVManager.sol | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 79de1c09f..4a2c81177 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -28,8 +28,7 @@ contract NAVManager is Auth { IAccounting public immutable accounting; IShareClassManager public immutable shareClassManager; - AccountId internal assetAccount; - AccountId internal expenseAccount; + AccountId internal nextAccountId; constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { require(hub.shareClassManager().shareClassCount(poolId_) == 1, InvalidShareClassCount()); @@ -49,7 +48,7 @@ contract NAVManager is Auth { hub.createAccount(poolId, gainAccount, false); hub.createAccount(poolId, lossAccount, false); - assetAccount = AccountId.wrap(5); + nextAccountId = AccountId.wrap(5); } //---------------------------------------------------------------------------------------------- @@ -57,15 +56,15 @@ contract NAVManager is Auth { //---------------------------------------------------------------------------------------------- function createHolding(AssetId assetId, IERC7726 valuation) external auth { - hub.createAccount(poolId, assetAccount, true); - hub.createHolding(poolId, scId, assetId, valuation, assetAccount, equityAccount, gainAccount, lossAccount); - assetAccount = assetAccount.increment(); + hub.createAccount(poolId, nextAccountId, true); + hub.createHolding(poolId, scId, assetId, valuation, nextAccountId, equityAccount, gainAccount, lossAccount); + nextAccountId = nextAccountId.increment(); } function createLiability(AssetId assetId, IERC7726 valuation) external auth { - hub.createAccount(poolId, expenseAccount, true); - hub.createLiability(poolId, scId, assetId, valuation, expenseAccount, liabilityAccount); - expenseAccount = expenseAccount.increment(); + hub.createAccount(poolId, nextAccountId, true); + hub.createLiability(poolId, scId, assetId, valuation, nextAccountId, liabilityAccount); + nextAccountId = nextAccountId.increment(); } //---------------------------------------------------------------------------------------------- From f5467af5ff8335eb885d69ae616e0b48b3b3bb50 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Mon, 5 May 2025 21:57:52 +0200 Subject: [PATCH 16/83] Remove loan manager code --- src/managers/LoansManager.sol | 185 ------------------------- src/misc/LinearAccrual.sol | 147 -------------------- src/misc/interfaces/ILinearAccrual.sol | 90 ------------ src/misc/libraries/Compounding.sol | 34 ----- src/misc/libraries/MathLib.sol | 14 -- src/misc/types/D18.sol | 14 +- 6 files changed, 2 insertions(+), 482 deletions(-) delete mode 100644 src/managers/LoansManager.sol delete mode 100644 src/misc/LinearAccrual.sol delete mode 100644 src/misc/interfaces/ILinearAccrual.sol delete mode 100644 src/misc/libraries/Compounding.sol diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol deleted file mode 100644 index edd59390a..000000000 --- a/src/managers/LoansManager.sol +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {Auth} from "src/misc/Auth.sol"; -import {IERC6909NFT} from "src/misc/interfaces/IERC6909.sol"; -import {ERC6909NFT} from "src/misc/ERC6909NFT.sol"; -import {D18, d18} from "src/misc/types/D18.sol"; -import {IERC7726} from "src/misc/interfaces/IERC7726.sol"; -import {ILinearAccrual} from "src/misc/interfaces/ILinearAccrual.sol"; - -import {MessageLib, UpdateContractType} from "src/common/libraries/MessageLib.sol"; -import {PoolId} from "src/common/types/PoolId.sol"; -import {AssetId, assetIdFromAddr} from "src/common/types/AssetId.sol"; -import {AccountId} from "src/common/types/AccountId.sol"; -import {ShareClassId} from "src/common/types/ShareClassId.sol"; - -import {IBalanceSheet} from "src/vaults/interfaces/IBalanceSheet.sol"; -import {IPoolManager} from "src/vaults/interfaces/IPoolManager.sol"; - -struct Loan { - // System properties - ShareClassId scId; - uint16 tokenId; - address borrower; - // Loan properties - address borrowAsset; - bytes32 rateId; - D18 maxBorrowAmount; - // Ongoing properties - int128 normalizedDebt; - D18 totalBorrowed; - D18 totalRepaid; -} - -// TODO: maturity date and/or open term - -contract LoansManager is Auth, IERC7726 { - error UnknownUpdateContractType(); - error UnregisteredRateId(); - error TooManyLoans(); - error NotTheOwner(); - error NonZeroOutstanding(); - error ExceedsLTV(); - - PoolId public immutable poolId; - AccountId public immutable equityAccount; - AccountId public immutable lossAccount; - AccountId public immutable gainAccount; - - IERC6909NFT public immutable token; - ILinearAccrual public immutable linearAccrual; - - IPoolManager public poolManager; - IBalanceSheet public balanceSheet; - - mapping(AssetId assetId => Loan) public loans; - - constructor( - PoolId poolId_, - IERC6909NFT token_, - ILinearAccrual linearAccrual_, - IPoolManager poolManager_, - IBalanceSheet balanceSheet_, - AccountId equityAccount_, - AccountId lossAccount_, - AccountId gainAccount_, - address deployer - ) Auth(deployer) { - poolId = poolId_; - equityAccount = equityAccount_; - lossAccount = lossAccount_; - gainAccount = gainAccount_; - - token = token_; - linearAccrual = linearAccrual_; - - poolManager = poolManager_; - balanceSheet = balanceSheet_; - } - - //---------------------------------------------------------------------------------------------- - // Owner actions - //---------------------------------------------------------------------------------------------- - - function update(PoolId, /* poolId */ ShareClassId, /* scId */ bytes calldata payload) external auth { - uint8 kind = uint8(MessageLib.updateContractType(payload)); - - if (kind == uint8(UpdateContractType.LoanMaxBorrowAmount)) { - MessageLib.UpdateContractLoanMaxBorrowAmount memory m = - MessageLib.deserializeUpdateContractLoanMaxBorrowAmount(payload); - - Loan storage loan = loans[AssetId.wrap(m.assetId)]; - require(linearAccrual.debt(loan.rateId, loan.normalizedDebt) <= int128(m.maxBorrowAmount), ExceedsLTV()); - - loan.maxBorrowAmount = d18(m.maxBorrowAmount); - // emit UpdateLoanMaxBorrowAmount(); - } else if (kind == uint8(UpdateContractType.LoanRate)) { - MessageLib.UpdateContractLoanRate memory m = MessageLib.deserializeUpdateContractLoanRate(payload); - - Loan storage loan = loans[AssetId.wrap(m.assetId)]; - - loan.normalizedDebt = linearAccrual.getRenormalizedDebt(loan.rateId, m.rateId, loan.normalizedDebt); - loan.rateId = m.rateId; - // emit UpdateLoanRate(); - } else { - revert UnknownUpdateContractType(); - } - } - - //---------------------------------------------------------------------------------------------- - // Borrower actions - //---------------------------------------------------------------------------------------------- - - function create(ShareClassId scId, address borrower, address borrowAsset, bytes32 rateId, string memory tokenURI) - external - { - require(linearAccrual.rateIdExists(rateId), UnregisteredRateId()); - - uint256 tokenId = token.mint(address(this), tokenURI); - require(tokenId <= type(uint16).max, TooManyLoans()); - AssetId assetId = poolManager.registerAsset(poolId.centrifugeId(), address(this), tokenId); - - loans[assetId] = Loan({ - scId: scId, - tokenId: uint16(tokenId), - borrower: borrower, - borrowAsset: borrowAsset, - rateId: rateId, - maxBorrowAmount: d18(0), - normalizedDebt: 0, - totalBorrowed: d18(0), - totalRepaid: d18(0) - }); - - balanceSheet.deposit(poolId, scId, address(this), uint16(tokenId), address(this), 1); - - // emit NewLoan(..); - } - - function borrow(AssetId assetId, uint128 amount, address receiver) external { - Loan storage loan = loans[assetId]; - require(loan.borrower == msg.sender, NotTheOwner()); - require( - linearAccrual.debt(loan.rateId, loan.normalizedDebt) + int128(amount) - <= int128(loan.maxBorrowAmount.inner()), - ExceedsLTV() - ); - - loan.normalizedDebt = linearAccrual.getModifiedNormalizedDebt(loan.rateId, loan.normalizedDebt, int128(amount)); - loan.totalBorrowed = loan.totalBorrowed + d18(amount); - - balanceSheet.withdraw(poolId, loan.scId, loan.borrowAsset, loan.tokenId, receiver, amount); - } - - function repay(AssetId assetId, uint128 amount, address owner) external { - Loan storage loan = loans[assetId]; - require(loan.borrower == msg.sender, NotTheOwner()); - - loan.normalizedDebt = linearAccrual.getModifiedNormalizedDebt(loan.rateId, loan.normalizedDebt, -int128(amount)); - loan.totalRepaid = loan.totalRepaid + d18(amount); - - balanceSheet.deposit(poolId, loan.scId, loan.borrowAsset, loan.tokenId, owner, amount); - } - - function close(AssetId assetId) external { - Loan storage loan = loans[assetId]; - require(loan.borrower == msg.sender, NotTheOwner()); - require(loan.normalizedDebt == 0, NonZeroOutstanding()); - - balanceSheet.withdraw(poolId, loan.scId, address(this), loan.tokenId, address(this), 1); - token.burn(loan.tokenId); - } - - //---------------------------------------------------------------------------------------------- - // Valuation - //---------------------------------------------------------------------------------------------- - - function getQuote(uint256, address base, address /* quote */ ) external view returns (uint256 quoteAmount) { - // TODO: how to know conversion to quote asset? - - Loan storage loan = loans[assetIdFromAddr(base)]; - int128 debt = linearAccrual.debt(loan.rateId, loan.normalizedDebt); - quoteAmount = debt > 0 ? uint256(uint128(debt)) : 0; - } -} diff --git a/src/misc/LinearAccrual.sol b/src/misc/LinearAccrual.sol deleted file mode 100644 index 6a6528d26..000000000 --- a/src/misc/LinearAccrual.sol +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {ILinearAccrual} from "src/misc/interfaces/ILinearAccrual.sol"; -import {Compounding, CompoundingPeriod} from "src/misc/libraries/Compounding.sol"; -import {MathLib} from "src/misc/libraries/MathLib.sol"; -import {d18, D18, mulUint128} from "src/misc/types/D18.sol"; - -contract LinearAccrual is ILinearAccrual { - using MathLib for uint128; - using MathLib for uint256; - using MathLib for int128; - - mapping(bytes32 rateId => Rate rate) public rates; - mapping(bytes32 rateId => Group group) public groups; - - //---------------------------------------------------------------------------------------------- - // Rate updates - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc ILinearAccrual - function drip(bytes32 rateId) public { - Rate storage rate = rates[rateId]; - - // Short circuit to save gas - if (rate.lastUpdated == uint64(block.timestamp)) { - return; - } - - Group memory group = groups[rateId]; - - // Determine number of full compounding periods passed since last update - uint64 periodsPassed = Compounding.getPeriodsPassed(group.period, rate.lastUpdated); - - if (periodsPassed > 0) { - rate.accumulatedRate = d18( - rate.accumulatedRate.mulUint128( - uint256(group.ratePerPeriod.inner()).rpow(periodsPassed, 1e18).toUint128(), MathLib.Rounding.Up - ) - ); - - emit RateAccumulated(rateId, rate.accumulatedRate.inner(), periodsPassed); - rate.lastUpdated = uint64(block.timestamp); - } - } - - //---------------------------------------------------------------------------------------------- - // Rate registration - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc ILinearAccrual - function registerRateId(uint128 ratePerPeriod_, CompoundingPeriod period) public returns (bytes32 rateId) { - D18 ratePerPeriod = d18(ratePerPeriod_); - Group memory group = Group(ratePerPeriod, period); - - rateId = keccak256(abi.encode(group)); - - require(rates[rateId].lastUpdated == 0, RateIdExists(rateId, ratePerPeriod.inner(), period)); - - groups[rateId] = group; - rates[rateId] = Rate(ratePerPeriod, uint64(block.timestamp)); - - emit NewRateId(rateId, ratePerPeriod.inner(), period); - } - - //---------------------------------------------------------------------------------------------- - // View methods - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc ILinearAccrual - function rateIdExists(bytes32 rateId) public view returns (bool) { - return rates[rateId].lastUpdated > 0; - } - - /// @inheritdoc ILinearAccrual - function getRateId(uint128 rate, CompoundingPeriod period) public pure returns (bytes32) { - Group memory group = Group(d18(rate), period); - - return keccak256(abi.encode(group)); - } - - /// @inheritdoc ILinearAccrual - function getModifiedNormalizedDebt(bytes32 rateId, int128 prevNormalizedDebt, int128 debtChange) - external - view - returns (int128 newNormalizedDebt) - { - _requireNonZeroUpdatedRateId(rateId); - - if (debtChange >= 0) { - return prevNormalizedDebt - + rates[rateId].accumulatedRate.reciprocalMulUint128(uint128(debtChange), MathLib.Rounding.Up).toInt128(); - } else { - return prevNormalizedDebt - - rates[rateId].accumulatedRate.reciprocalMulUint128(uint128(-debtChange), MathLib.Rounding.Up).toInt128(); - } - } - - /// @inheritdoc ILinearAccrual - function getRenormalizedDebt(bytes32 oldRateId, bytes32 newRateId, int128 prevNormalizedDebt) - external - view - returns (int128 newNormalizedDebt) - { - _requireNonZeroUpdatedRateId(newRateId); - - int128 debt_ = debt(oldRateId, prevNormalizedDebt); - - if (debt_ >= 0) { - return rates[newRateId].accumulatedRate.reciprocalMulUint128( - debt_.toUint256().toUint128(), MathLib.Rounding.Up - ).toInt128(); - } else { - return -( - rates[newRateId].accumulatedRate.reciprocalMulUint128( - (-debt_).toUint256().toUint128(), MathLib.Rounding.Up - ).toInt128() - ); - } - } - - /// @inheritdoc ILinearAccrual - function debt(bytes32 rateId, int128 normalizedDebt) public view returns (int128) { - _requireNonZeroUpdatedRateId(rateId); - - // Casting to int128 safe because we don't exceed number of digits of normalizedDebt - // Casting to uint256 necessary for mulDiv - if (normalizedDebt >= 0) { - return normalizedDebt.toUint256().mulDiv(rates[rateId].accumulatedRate.inner(), 1e18).toUint128().toInt128(); - } else { - return -(-normalizedDebt).toUint256().mulDiv(rates[rateId].accumulatedRate.inner(), 1e18).toUint128().toInt128(); - } - } - - //---------------------------------------------------------------------------------------------- - // Internal methods - //---------------------------------------------------------------------------------------------- - - /// @notice Ensures the given rate id was updated in the current block and is not the zero-rate. - /// @dev Throws if rate has not been updated in the current block - /// @dev Throws if rate is zero-rate - /// @param rateId Identifier of the rate group - function _requireNonZeroUpdatedRateId(bytes32 rateId) internal view { - require(rates[rateId].lastUpdated != 0 && rates[rateId].accumulatedRate.inner() != 0, RateIdMissing(rateId)); - require(rates[rateId].lastUpdated == block.timestamp, RateIdOutdated(rateId, rates[rateId].lastUpdated)); - } -} diff --git a/src/misc/interfaces/ILinearAccrual.sol b/src/misc/interfaces/ILinearAccrual.sol deleted file mode 100644 index 669a26d11..000000000 --- a/src/misc/interfaces/ILinearAccrual.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity >=0.5.0; - -import {CompoundingPeriod} from "src/misc/libraries/Compounding.sol"; -import {D18} from "src/misc/types/D18.sol"; - -interface ILinearAccrual { - /// @dev Represents the rate accumulator and the timestamp of the last rate update - struct Rate { - /// @dev Accumulated rate index over time - /// @dev Assumes rate to be prefixed by 1, i.e. 5% rate shall be represented as 1.05 = d18(1e18 + 5e16) - D18 accumulatedRate; - /// @dev Timestamp of last rate update - uint64 lastUpdated; - } - - /// @dev Each group corresponds to a particular compound period and the accrual rate per period - struct Group { - /// @dev Rate per compound period - /// @dev Assumes rate to be prefixed by 1, i.e. 5% rate shall be represented as 1.05 = d18(1e18 + 5e16) - D18 ratePerPeriod; - /// @dev Duration of compound period - CompoundingPeriod period; - } - - /// Events - event NewRateId(bytes32 indexed rateId, uint128 indexed ratePerPeriod, CompoundingPeriod period); - event RateAccumulated(bytes32 indexed rateId, uint128 indexed rate, uint64 periodsPassed); - - /// Errors - error RateIdExists(bytes32 rateId, uint128 ratePerPeriod, CompoundingPeriod period); - error RateIdMissing(bytes32 rateId); - error RateIdOutdated(bytes32 rateId, uint64 lastUpdated); - - /// @notice Updates the accumulated rate of the corresponding identifier based on the periods which have passed - /// since the last update - /// @param rateId the id of the interest rate group - function drip(bytes32 rateId) external; - - /// @notice Registers the rate identifier for the given rate and compound period and returns it. - /// @dev Throws if rate has been updated once already implying it has been registered before - /// - /// @param ratePerPeriod Rate per compound period - /// @param period Compounding schedule - function registerRateId(uint128 ratePerPeriod, CompoundingPeriod period) external returns (bytes32 rateId); - - /// @notice Returns whether the rate identifier has been regsitered. - /// - /// @param rateId the id of the interest rate group - function rateIdExists(bytes32 rateId) external view returns (bool); - - /// @notice Returns the rate identifier for the given rate and compound period. - /// - /// @param ratePerPeriod Rate per compound period - /// @param period Compounding schedule - function getRateId(uint128 ratePerPeriod, CompoundingPeriod period) external pure returns (bytes32 rateId); - - /// @notice Returns the sum of the current normalized debt and the normalized change. - /// @dev Throws if rate has not been updated in the current block - /// @dev Throws if rate is zero-rate - /// - /// @param rateId Identifier of the rate group - /// @param prevNormalizedDebt Normalized debt before decreasing - /// @param debtChange The amount by which we modify the debt - function getModifiedNormalizedDebt(bytes32 rateId, int128 prevNormalizedDebt, int128 debtChange) - external - view - returns (int128 newNormalizedDebt); - - /// @notice Returns the renormalized debt based on the current rate group after transitioning normalization from - /// the previous one. - /// @dev Throws if rate has not been updated in the current block - /// @dev Throws if rate is zero-rate - /// - /// @param oldRateId Identifier of the previous rate group - /// @param newRateId Identifier of the current rate group - /// @param prevNormalizedDebt Normalized debt under previous rate group - function getRenormalizedDebt(bytes32 oldRateId, bytes32 newRateId, int128 prevNormalizedDebt) - external - view - returns (int128 newNormalizedDebt); - - /// @notice Returns the current debt without normalization based on actual block.timestamp (now) and the - /// accumulated rate. - /// @dev Throws if rate has not been updated in the current block - /// @dev Throws if rate is zero-rate - /// @param rateId Identifier of the rate group - /// @param normalizedDebt Normalized debt from which we derive the unnormalized debt - function debt(bytes32 rateId, int128 normalizedDebt) external view returns (int128 unnormalizedDebt); -} diff --git a/src/misc/libraries/Compounding.sol b/src/misc/libraries/Compounding.sol deleted file mode 100644 index 5e6ee1367..000000000 --- a/src/misc/libraries/Compounding.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -enum CompoundingPeriod { - Secondly, - Daily -} - -library Compounding { - uint64 constant SECONDS_PER_DAY = 86400; // 60 * 60 * 24 - - /// @notice Returns the amount of seconds for the given compounding period. - /// - /// @dev Default case is `CompoundingPeriod.Daily`. - function getSeconds(CompoundingPeriod period) public pure returns (uint64) { - if (period == CompoundingPeriod.Daily) return SECONDS_PER_DAY; - else return 1; - } - - /// @notice Returns the number of full compounding periods that have passed since a given timestamp. - /// - /// @dev Default case is `CompoundingPeriod.Daily` and returns 0 for any given future timestamp. - function getPeriodsPassed(CompoundingPeriod period, uint64 startTimestamp) public view returns (uint64) { - if (startTimestamp >= block.timestamp) { - return 0; - } else if (period == CompoundingPeriod.Daily) { - uint64 startDay = startTimestamp / SECONDS_PER_DAY; - uint64 nowDay = uint64(block.timestamp) / SECONDS_PER_DAY; - return nowDay - startDay; - } else { - return uint64(block.timestamp) - startTimestamp; - } - } -} diff --git a/src/misc/libraries/MathLib.sol b/src/misc/libraries/MathLib.sol index 222edf591..edc8eb5c1 100644 --- a/src/misc/libraries/MathLib.sol +++ b/src/misc/libraries/MathLib.sol @@ -170,26 +170,12 @@ library MathLib { return uint64(value); } - /// @notice Safe type conversion from uint128 to int128 - function toInt128(uint128 _value) internal pure returns (int128) { - require(_value <= uint128(type(int128).max), "MathLib/uint128-to-int128-overflow"); - - return int128(_value); - } /// @notice Safe type conversion from uint256 to uint128. - function toUint128(uint256 value) internal pure returns (uint128) { require(value <= type(uint128).max, Uint128_Overflow()); return uint128(value); } - /// @notice Safe type conversion from int128 to uint128 - function toUint256(int128 _value) internal pure returns (uint256) { - require(_value >= 0, "MathLib/int128-to-uint256-underflow"); - - return uint256(int256(_value)); - } - /// @notice Returns the smallest of two numbers. function min(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? b : a; diff --git a/src/misc/types/D18.sol b/src/misc/types/D18.sol index 9195b6b92..2ff93b120 100644 --- a/src/misc/types/D18.sol +++ b/src/misc/types/D18.sol @@ -92,18 +92,10 @@ function d18(uint128 num, uint128 den) pure returns (D18) { return D18.wrap(MathLib.mulDiv(num, 1e18, den).toUint128()); } -function isNull(D18 d) pure returns (bool) { - return D18.unwrap(d) == 0; -} - function eq(D18 a, D18 b) pure returns (bool) { return D18.unwrap(a) == D18.unwrap(b); } -function lte(D18 a, D18 b) pure returns (bool) { - return D18.unwrap(a) <= D18.unwrap(b); -} - function raw(D18 d) pure returns (uint128) { return D18.unwrap(d); } @@ -112,7 +104,6 @@ using { add as +, sub as -, divD18 as /, - lte as <=, inner, eq, mulD18 as *, @@ -121,6 +112,5 @@ using { reciprocalMulUint128, reciprocalMulUint256, reciprocal, - raw, - isNull -} for D18 global; + raw +} for D18 global; \ No newline at end of file From 25de08e81cc35b9cd28bb15a611b6ead5d0ad173 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Mon, 5 May 2025 21:59:15 +0200 Subject: [PATCH 17/83] More removals --- src/common/libraries/MessageLib.sol | 50 +------------ src/common/types/AssetId.sol | 4 -- src/misc/ERC6909.sol | 107 ---------------------------- src/misc/ERC6909NFT.sol | 52 -------------- src/misc/types/D18.sol | 2 +- 5 files changed, 2 insertions(+), 213 deletions(-) delete mode 100644 src/misc/ERC6909.sol delete mode 100644 src/misc/ERC6909NFT.sol diff --git a/src/common/libraries/MessageLib.sol b/src/common/libraries/MessageLib.sol index 59d4d0fd1..0bf110f4e 100644 --- a/src/common/libraries/MessageLib.sol +++ b/src/common/libraries/MessageLib.sol @@ -66,9 +66,7 @@ enum UpdateContractType { MaxAssetPriceAge, MaxSharePriceAge, Valuation, - SyncDepositMaxReserve, - LoanMaxBorrowAmount, - LoanRate + SyncDepositMaxReserve } /// @dev Used internally in the VaultUpdateMessage (not represent a submessage) @@ -735,52 +733,6 @@ library MessageLib { return abi.encodePacked(UpdateContractType.SyncDepositMaxReserve, t.assetId, t.maxReserve); } - //--------------------------------------- - // UpdateContract.LoanMaxBorrowAmount (submsg) - //--------------------------------------- - - struct UpdateContractLoanMaxBorrowAmount { - uint128 assetId; - uint128 maxBorrowAmount; - } - - function deserializeUpdateContractLoanMaxBorrowAmount(bytes memory data) - internal - pure - returns (UpdateContractLoanMaxBorrowAmount memory) - { - require(updateContractType(data) == UpdateContractType.LoanMaxBorrowAmount, UnknownMessageType()); - - return UpdateContractLoanMaxBorrowAmount({assetId: data.toUint128(1), maxBorrowAmount: data.toUint128(17)}); - } - - function serialize(UpdateContractLoanMaxBorrowAmount memory t) internal pure returns (bytes memory) { - return abi.encodePacked(UpdateContractType.LoanMaxBorrowAmount, t.assetId, t.maxBorrowAmount); - } - - //--------------------------------------- - // UpdateContract.LoanRate (submsg) - //--------------------------------------- - - struct UpdateContractLoanRate { - uint128 assetId; - bytes32 rateId; - } - - function deserializeUpdateContractLoanRate(bytes memory data) - internal - pure - returns (UpdateContractLoanRate memory) - { - require(updateContractType(data) == UpdateContractType.LoanRate, UnknownMessageType()); - - return UpdateContractLoanRate({assetId: data.toUint128(1), rateId: data.toBytes32(17)}); - } - - function serialize(UpdateContractLoanRate memory t) internal pure returns (bytes memory) { - return abi.encodePacked(UpdateContractType.LoanRate, t.assetId, t.rateId); - } - //--------------------------------------- // DepositRequest //--------------------------------------- diff --git a/src/common/types/AssetId.sol b/src/common/types/AssetId.sol index e9d94c2c3..75ffa7fa3 100644 --- a/src/common/types/AssetId.sol +++ b/src/common/types/AssetId.sol @@ -13,10 +13,6 @@ function addr(AssetId assetId) pure returns (address) { return address(uint160(AssetId.unwrap(assetId))); } -function assetIdFromAddr(address value) pure returns (AssetId assetId) { - return AssetId.wrap(uint128(uint160(value))); -} - function raw(AssetId assetId) pure returns (uint128) { return AssetId.unwrap(assetId); } diff --git a/src/misc/ERC6909.sol b/src/misc/ERC6909.sol deleted file mode 100644 index fc7f55423..000000000 --- a/src/misc/ERC6909.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {IERC165} from "forge-std/interfaces/IERC165.sol"; - -import {IERC6909} from "src/misc/interfaces/IERC6909.sol"; - -/// @title Basic implementation of all properties according to the ERC6909. -/// -/// @dev This implementation MUST be extended with another contract which defines how tokens are created. -/// Either implement mint/burn or override transfer/transferFrom. -abstract contract ERC6909 is IERC6909 { - mapping(address owner => mapping(uint256 tokenId => uint256)) public balanceOf; - mapping(address owner => mapping(address operator => bool)) public isOperator; - mapping(address owner => mapping(address spender => mapping(uint256 tokenId => uint256))) public allowance; - - /// @inheritdoc IERC6909 - function transfer(address receiver, uint256 tokenId, uint256 amount) external virtual returns (bool) { - return _transfer(msg.sender, receiver, tokenId, amount); - } - - /// @inheritdoc IERC6909 - function transferFrom(address sender, address receiver, uint256 tokenId, uint256 amount) - external - virtual - returns (bool) - { - return _transferFrom(msg.sender, sender, receiver, tokenId, amount); - } - - /// @inheritdoc IERC6909 - function approve(address spender, uint256 tokenId, uint256 amount) external returns (bool) { - allowance[msg.sender][spender][tokenId] = amount; - - emit Approval(msg.sender, spender, tokenId, amount); - - return true; - } - - /// @inheritdoc IERC6909 - function setOperator(address operator, bool approved) external returns (bool) { - isOperator[msg.sender][operator] = approved; - - emit OperatorSet(msg.sender, operator, approved); - - return true; - } - - /// @inheritdoc IERC165 - function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { - return type(IERC6909).interfaceId == interfaceId || type(IERC165).interfaceId == interfaceId; - } - - function _mint(address owner, uint256 tokenId, uint256 amount) internal { - require(owner != address(0), EmptyOwner()); - require(tokenId > 0, InvalidTokenId()); - require(amount > 0, EmptyAmount()); - - balanceOf[owner][tokenId] += amount; - - emit Transfer(msg.sender, address(0), owner, tokenId, amount); - } - - function _burn(address owner, uint256 tokenId, uint256 amount) internal { - uint256 balance = balanceOf[owner][tokenId]; - require(balance >= amount, InsufficientBalance(msg.sender, tokenId)); - - // The underflow check is handled by the require line above - unchecked { - balanceOf[owner][tokenId] -= amount; - } - - emit Transfer(owner, owner, address(0), tokenId, amount); - } - - function _transferFrom(address spender, address sender, address receiver, uint256 tokenId, uint256 amount) - internal - returns (bool) - { - if (spender != sender && !isOperator[sender][spender]) { - uint256 allowed = allowance[sender][spender][tokenId]; - if (allowed != type(uint256).max) { - require(amount <= allowed, InsufficientAllowance(spender, tokenId)); - allowance[sender][spender][tokenId] -= amount; - } - } - - return _transfer(sender, receiver, tokenId, amount); - } - - function _transfer(address sender, address receiver, uint256 tokenId, uint256 amount) internal returns (bool) { - uint256 senderBalance = balanceOf[sender][tokenId]; - require(senderBalance >= amount, InsufficientBalance(sender, tokenId)); - - // The require check few lines above guarantees that - // it cannot underflow. - unchecked { - balanceOf[sender][tokenId] -= amount; - } - - balanceOf[receiver][tokenId] += amount; - - emit Transfer(msg.sender, sender, receiver, tokenId, amount); - - return true; - } -} diff --git a/src/misc/ERC6909NFT.sol b/src/misc/ERC6909NFT.sol deleted file mode 100644 index 00ff04074..000000000 --- a/src/misc/ERC6909NFT.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {ERC6909} from "src/misc/ERC6909.sol"; -import {StringLib} from "src/misc/libraries/StringLib.sol"; -import {Auth} from "src/misc/Auth.sol"; -import {IERC6909NFT, IERC6909URIExt} from "src/misc/interfaces/IERC6909.sol"; - -contract ERC6909NFT is ERC6909, Auth, IERC6909NFT { - using StringLib for string; - - uint8 constant MAX_SUPPLY = 1; - - uint256 public latestTokenId; - - /// @inheritdoc IERC6909URIExt - string public contractURI; - /// @inheritdoc IERC6909URIExt - mapping(uint256 tokenId => string URI) public tokenURI; - - constructor(address deployer) Auth(deployer) {} - - /// @inheritdoc IERC6909NFT - function setTokenURI(uint256 tokenId, string memory URI) public auth { - tokenURI[tokenId] = URI; - - emit TokenURISet(tokenId, URI); - } - - /// @inheritdoc IERC6909NFT - function mint(address owner, string memory tokenURI_) public auth returns (uint256 tokenId) { - require(!tokenURI_.isEmpty(), EmptyURI()); - - tokenId = ++latestTokenId; - - _mint(owner, tokenId, MAX_SUPPLY); - - setTokenURI(tokenId, tokenURI_); - } - - /// @inheritdoc IERC6909NFT - function burn(uint256 tokenId) external { - _burn(msg.sender, tokenId, 1); - } - - // @inheritdoc IERC6909NFT - function setContractURI(string calldata URI) external auth { - contractURI = URI; - - emit ContractURISet(address(this), URI); - } -} diff --git a/src/misc/types/D18.sol b/src/misc/types/D18.sol index 2ff93b120..f1219d9fb 100644 --- a/src/misc/types/D18.sol +++ b/src/misc/types/D18.sol @@ -113,4 +113,4 @@ using { reciprocalMulUint256, reciprocal, raw -} for D18 global; \ No newline at end of file +} for D18 global; From b69fd5b1cbb7591926659c40a34b115f6cb8d43c Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 4 Jun 2025 09:04:48 +0200 Subject: [PATCH 18/83] Start to adapt for multiple networks --- src/common/types/AccountId.sol | 10 +-- src/managers/NAVManager.sol | 130 ++++++++++++++++++++++----------- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/common/types/AccountId.sol b/src/common/types/AccountId.sol index d2f1121a0..38843d0b0 100644 --- a/src/common/types/AccountId.sol +++ b/src/common/types/AccountId.sol @@ -7,12 +7,12 @@ function raw(AccountId accountId_) pure returns (uint32) { return AccountId.unwrap(accountId_); } -function increment(AccountId accountId_) pure returns (AccountId) { - return AccountId.wrap(AccountId.unwrap(accountId_) + 1); -} - function neq(AccountId a, AccountId b) pure returns (bool) { return AccountId.unwrap(a) != AccountId.unwrap(b); } -using {raw, increment, neq as !=} for AccountId global; +function withCentrifugeId(uint16 centrifugeId, uint16 index) pure returns (AccountId) { + return AccountId.wrap((uint32(centrifugeId) << 16) | uint32(index)); +} + +using {raw, neq as !=} for AccountId global; diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 06ea6a0ab..3de9648db 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -6,30 +6,29 @@ import {D18, d18} from "src/misc/types/D18.sol"; import {PoolId} from "src/common/types/PoolId.sol"; import {AssetId} from "src/common/types/AssetId.sol"; -import {AccountId} from "src/common/types/AccountId.sol"; +import {AccountId, withCentrifugeId} from "src/common/types/AccountId.sol"; import {ShareClassId} from "src/common/types/ShareClassId.sol"; import {IValuation} from "src/common/interfaces/IValuation.sol"; +import {ISnapshotHook} from "src/common/interfaces/ISnapshotHook.sol"; import {IHub} from "src/hub/interfaces/IHub.sol"; import {IAccounting} from "src/hub/interfaces/IAccounting.sol"; import {IShareClassManager} from "src/hub/interfaces/IShareClassManager.sol"; -contract NAVManager is Auth { +contract NAVManager is Auth, ISnapshotHook { error InvalidShareClassCount(); + error AlreadyInitialized(); + error NotInitialized(); + error ExceedsMaxAccounts(); PoolId public immutable poolId; ShareClassId public immutable scId; - AccountId public immutable equityAccount; - AccountId public immutable liabilityAccount; - AccountId public immutable gainAccount; - AccountId public immutable lossAccount; - IHub public immutable hub; IAccounting public immutable accounting; IShareClassManager public immutable shareClassManager; - AccountId internal nextAccountId; + mapping(uint16 centrifugeId => uint16) public accountCounter; constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { require(hub.shareClassManager().shareClassCount(poolId_) == 1, InvalidShareClassCount()); @@ -39,71 +38,120 @@ contract NAVManager is Auth { hub = hub_; accounting = hub.accounting(); shareClassManager = hub.shareClassManager(); - - equityAccount = AccountId.wrap(1); - liabilityAccount = AccountId.wrap(2); - gainAccount = AccountId.wrap(3); - lossAccount = AccountId.wrap(4); - hub.createAccount(poolId, equityAccount, false); - hub.createAccount(poolId, liabilityAccount, false); - hub.createAccount(poolId, gainAccount, false); - hub.createAccount(poolId, lossAccount, false); - - nextAccountId = AccountId.wrap(5); } //---------------------------------------------------------------------------------------------- // Account creation //---------------------------------------------------------------------------------------------- - // TODO: create equity/gain/loss/liability accounts per centrifugeId - // TODO: add ISnapshotHook.onSync + function initializeNetwork(uint16 centrifugeId) external auth { + require(accountCounter[centrifugeId] == 0, AlreadyInitialized()); + + hub.createAccount(poolId, equityAccount(centrifugeId), false); // equity + hub.createAccount(poolId, liabilityAccount(centrifugeId), false); // liability + hub.createAccount(poolId, gainAccount(centrifugeId), false); // gain + hub.createAccount(poolId, lossAccount(centrifugeId), false); // loss + + accountCounter[centrifugeId] = 5; + } function initializeHolding(AssetId assetId, IValuation valuation) external auth { - hub.createAccount(poolId, nextAccountId, true); - hub.initializeHolding(poolId, scId, assetId, valuation, nextAccountId, equityAccount, gainAccount, lossAccount); - nextAccountId = nextAccountId.increment(); + uint16 centrifugeId = assetId.centrifugeId(); + uint16 index = accountCounter[centrifugeId]; + require(index > 0, NotInitialized()); + require(index < type(uint16).max, ExceedsMaxAccounts()); + + AccountId assetAccount = withCentrifugeId(centrifugeId, index); + hub.createAccount(poolId, assetAccount, true); + hub.initializeHolding( + poolId, + scId, + assetId, + valuation, + assetAccount, + equityAccount(centrifugeId), + gainAccount(centrifugeId), + lossAccount(centrifugeId) + ); + + accountCounter[centrifugeId] = index + 1; } function initializeLiability(AssetId assetId, IValuation valuation) external auth { - hub.createAccount(poolId, nextAccountId, true); - hub.initializeLiability(poolId, scId, assetId, valuation, nextAccountId, liabilityAccount); - nextAccountId = nextAccountId.increment(); + uint16 centrifugeId = assetId.centrifugeId(); + uint16 index = accountCounter[centrifugeId]; + require(index > 0, NotInitialized()); + require(index < type(uint16).max, ExceedsMaxAccounts()); + + AccountId expenseAccount = withCentrifugeId(centrifugeId, index); + hub.createAccount(poolId, expenseAccount, true); + hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount, liabilityAccount(centrifugeId)); + + accountCounter[centrifugeId] = index + 1; } //---------------------------------------------------------------------------------------------- // Price updates //---------------------------------------------------------------------------------------------- + /// @inheritdoc ISnapshotHook + function onSync(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId) external { + // TODO + require(poolId == poolId_ && scId == scId_); + + D18 price = navPoolPerShare(centrifugeId); + + // TODO: combine with + + hub.updateSharePrice(poolId, scId, price); + } + function updateHoldingValue(AssetId assetId) external { hub.updateHoldingValue(poolId, scId, assetId); } - function updatePricePerShare() external { - (D18 current,) = navPoolPerShare(); - hub.updateSharePrice(poolId, scId, current); - } + // TODO: setHoldingAccountId, updateHoldingValuation + // TODO: realize gain/loss to move to equity account //---------------------------------------------------------------------------------------------- // Calculations //---------------------------------------------------------------------------------------------- /// @dev NAV = equity + gain - loss - liability - function netAssetValue() public view returns (D18) { + function netAssetValue(uint16 centrifugeId) public view returns (D18) { // TODO: how to handle when one of the accounts is not positive - (, uint128 equity) = accounting.accountValue(poolId, equityAccount); - (, uint128 gain) = accounting.accountValue(poolId, gainAccount); - (, uint128 loss) = accounting.accountValue(poolId, lossAccount); - (, uint128 liability) = accounting.accountValue(poolId, liabilityAccount); + (, uint128 equity) = accounting.accountValue(poolId, equityAccount(centrifugeId)); + (, uint128 gain) = accounting.accountValue(poolId, gainAccount(centrifugeId)); + (, uint128 loss) = accounting.accountValue(poolId, lossAccount(centrifugeId)); + (, uint128 liability) = accounting.accountValue(poolId, liabilityAccount(centrifugeId)); return d18(equity) + d18(gain) - d18(loss) - d18(liability); } /// @dev Price = NAV / share class issuance - function navPoolPerShare() public view returns (D18 current, D18 stored) { - D18 nav = netAssetValue(); - (uint128 issuance, D18 prev) = shareClassManager.metrics(scId); + function navPoolPerShare(uint16 centrifugeId) public view returns (D18) { + D18 nav = netAssetValue(centrifugeId); + uint128 issuance = shareClassManager.issuance(scId, centrifugeId); + + return nav / d18(issuance); + } + + //---------------------------------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------------------------------- + + function equityAccount(uint16 centrifugeId) public pure returns (AccountId) { + return equityAccount(centrifugeId); + } + + function liabilityAccount(uint16 centrifugeId) public pure returns (AccountId) { + return liabilityAccount(centrifugeId); + } + + function gainAccount(uint16 centrifugeId) public pure returns (AccountId) { + return gainAccount(centrifugeId); + } - current = nav / d18(issuance); - stored = prev; + function lossAccount(uint16 centrifugeId) public pure returns (AccountId) { + return lossAccount(centrifugeId); } } From 755944b076bdcb1219257e29f6c6534ef3c9d8d1 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Wed, 4 Jun 2025 09:20:52 +0200 Subject: [PATCH 19/83] Make it work --- src/common/types/PoolId.sol | 6 +++++- src/managers/NAVManager.sol | 41 ++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/common/types/PoolId.sol b/src/common/types/PoolId.sol index abebe75e4..456e44c08 100644 --- a/src/common/types/PoolId.sol +++ b/src/common/types/PoolId.sol @@ -19,8 +19,12 @@ function isNull(PoolId poolId) pure returns (bool) { return PoolId.unwrap(poolId) == 0; } +function equals(PoolId a, PoolId b) pure returns (bool) { + return PoolId.unwrap(a) == PoolId.unwrap(b); +} + function raw(PoolId poolId) pure returns (uint64) { return PoolId.unwrap(poolId); } -using {centrifugeId, isNull, raw} for PoolId global; +using {centrifugeId, isNull, raw, equals as ==} for PoolId global; diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 3de9648db..ec831f029 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -15,6 +15,11 @@ import {IHub} from "src/hub/interfaces/IHub.sol"; import {IAccounting} from "src/hub/interfaces/IAccounting.sol"; import {IShareClassManager} from "src/hub/interfaces/IShareClassManager.sol"; +struct NetworkMetrics { + D18 netAssetValue; + uint128 issuance; +} + contract NAVManager is Auth, ISnapshotHook { error InvalidShareClassCount(); error AlreadyInitialized(); @@ -25,17 +30,22 @@ contract NAVManager is Auth, ISnapshotHook { ShareClassId public immutable scId; IHub public immutable hub; + address public immutable holdings; IAccounting public immutable accounting; IShareClassManager public immutable shareClassManager; + uint128 public globalIssuance; + D18 public globalNetAssetValue; mapping(uint16 centrifugeId => uint16) public accountCounter; + mapping(uint16 centrifugeId => NetworkMetrics) public metrics; - constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { + constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address holdings_, address deployer) Auth(deployer) { require(hub.shareClassManager().shareClassCount(poolId_) == 1, InvalidShareClassCount()); poolId = poolId_; scId = scId_; hub = hub_; + holdings = holdings_; accounting = hub.accounting(); shareClassManager = hub.shareClassManager(); } @@ -47,10 +57,10 @@ contract NAVManager is Auth, ISnapshotHook { function initializeNetwork(uint16 centrifugeId) external auth { require(accountCounter[centrifugeId] == 0, AlreadyInitialized()); - hub.createAccount(poolId, equityAccount(centrifugeId), false); // equity - hub.createAccount(poolId, liabilityAccount(centrifugeId), false); // liability - hub.createAccount(poolId, gainAccount(centrifugeId), false); // gain - hub.createAccount(poolId, lossAccount(centrifugeId), false); // loss + hub.createAccount(poolId, equityAccount(centrifugeId), false); + hub.createAccount(poolId, liabilityAccount(centrifugeId), false); + hub.createAccount(poolId, gainAccount(centrifugeId), false); + hub.createAccount(poolId, lossAccount(centrifugeId), false); accountCounter[centrifugeId] = 5; } @@ -96,12 +106,19 @@ contract NAVManager is Auth, ISnapshotHook { /// @inheritdoc ISnapshotHook function onSync(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId) external { - // TODO require(poolId == poolId_ && scId == scId_); + require(msg.sender == holdings, NotAuthorized()); + + NetworkMetrics storage networkMetrics = metrics[centrifugeId]; + D18 netAssetValue_ = netAssetValue(centrifugeId); + uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - D18 price = navPoolPerShare(centrifugeId); + globalIssuance = globalIssuance + issuance - networkMetrics.issuance; + globalNetAssetValue = globalNetAssetValue + netAssetValue_ - networkMetrics.netAssetValue; + D18 price = globalNetAssetValue / d18(globalIssuance); - // TODO: combine with + networkMetrics.netAssetValue = netAssetValue_; + networkMetrics.issuance = issuance; hub.updateSharePrice(poolId, scId, price); } @@ -127,12 +144,8 @@ contract NAVManager is Auth, ISnapshotHook { return d18(equity) + d18(gain) - d18(loss) - d18(liability); } - /// @dev Price = NAV / share class issuance - function navPoolPerShare(uint16 centrifugeId) public view returns (D18) { - D18 nav = netAssetValue(centrifugeId); - uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - - return nav / d18(issuance); + function navPoolPerShare() public view returns (D18) { + return globalNetAssetValue / d18(globalIssuance); } //---------------------------------------------------------------------------------------------- From b88f8350153714ba409d0758efde06fae81f040a Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 11:06:36 +0200 Subject: [PATCH 20/83] Separate price calculations --- src/managers/NAVManager.sol | 47 ++++++++----------- src/managers/SimplePriceManager.sol | 71 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 28 deletions(-) create mode 100644 src/managers/SimplePriceManager.sol diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index ec831f029..d39ad693e 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -13,13 +13,13 @@ import {ISnapshotHook} from "src/common/interfaces/ISnapshotHook.sol"; import {IHub} from "src/hub/interfaces/IHub.sol"; import {IAccounting} from "src/hub/interfaces/IAccounting.sol"; -import {IShareClassManager} from "src/hub/interfaces/IShareClassManager.sol"; -struct NetworkMetrics { - D18 netAssetValue; - uint128 issuance; +interface INAVHook { + /// @notice Callback when there is a new net asset value (NAV) on a specific network. + function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external; } +/// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is Auth, ISnapshotHook { error InvalidShareClassCount(); error AlreadyInitialized(); @@ -32,22 +32,25 @@ contract NAVManager is Auth, ISnapshotHook { IHub public immutable hub; address public immutable holdings; IAccounting public immutable accounting; - IShareClassManager public immutable shareClassManager; - uint128 public globalIssuance; - D18 public globalNetAssetValue; + INAVHook public navHook; mapping(uint16 centrifugeId => uint16) public accountCounter; - mapping(uint16 centrifugeId => NetworkMetrics) public metrics; - - constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address holdings_, address deployer) Auth(deployer) { - require(hub.shareClassManager().shareClassCount(poolId_) == 1, InvalidShareClassCount()); + constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { poolId = poolId_; scId = scId_; + hub = hub_; - holdings = holdings_; + holdings = address(hub.holdings()); accounting = hub.accounting(); - shareClassManager = hub.shareClassManager(); + } + + //---------------------------------------------------------------------------------------------- + // Administration + //---------------------------------------------------------------------------------------------- + + function setNAVHook(INAVHook navHook_) external auth { + navHook = navHook_; } //---------------------------------------------------------------------------------------------- @@ -73,6 +76,7 @@ contract NAVManager is Auth, ISnapshotHook { AccountId assetAccount = withCentrifugeId(centrifugeId, index); hub.createAccount(poolId, assetAccount, true); + // TOOD: should be adapted to asset account and holding per scId hub.initializeHolding( poolId, scId, @@ -95,6 +99,7 @@ contract NAVManager is Auth, ISnapshotHook { AccountId expenseAccount = withCentrifugeId(centrifugeId, index); hub.createAccount(poolId, expenseAccount, true); + // TOOD: should be adapted to expense account and liability per scId hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount, liabilityAccount(centrifugeId)); accountCounter[centrifugeId] = index + 1; @@ -109,18 +114,8 @@ contract NAVManager is Auth, ISnapshotHook { require(poolId == poolId_ && scId == scId_); require(msg.sender == holdings, NotAuthorized()); - NetworkMetrics storage networkMetrics = metrics[centrifugeId]; D18 netAssetValue_ = netAssetValue(centrifugeId); - uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - - globalIssuance = globalIssuance + issuance - networkMetrics.issuance; - globalNetAssetValue = globalNetAssetValue + netAssetValue_ - networkMetrics.netAssetValue; - D18 price = globalNetAssetValue / d18(globalIssuance); - - networkMetrics.netAssetValue = netAssetValue_; - networkMetrics.issuance = issuance; - - hub.updateSharePrice(poolId, scId, price); + navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); } function updateHoldingValue(AssetId assetId) external { @@ -144,10 +139,6 @@ contract NAVManager is Auth, ISnapshotHook { return d18(equity) + d18(gain) - d18(loss) - d18(liability); } - function navPoolPerShare() public view returns (D18) { - return globalNetAssetValue / d18(globalIssuance); - } - //---------------------------------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------------------------------- diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol new file mode 100644 index 000000000..bcd14635c --- /dev/null +++ b/src/managers/SimplePriceManager.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {D18, d18} from "src/misc/types/D18.sol"; + +import {PoolId} from "src/common/types/PoolId.sol"; +import {ShareClassId} from "src/common/types/ShareClassId.sol"; + +import {IHub} from "src/hub/interfaces/IHub.sol"; +import {IShareClassManager} from "src/hub/interfaces/IShareClassManager.sol"; + +import {INAVHook} from "src/managers/NAVManager.sol"; + +struct NetworkMetrics { + D18 netAssetValue; + uint128 issuance; +} + +/// @notice Share price calculation manager for single share class pools. +contract SimplePriceManager is INAVHook { + error InvalidShareClassCount(); + + PoolId public immutable poolId; + ShareClassId public immutable scId; + + IHub public immutable hub; + IShareClassManager public immutable shareClassManager; + + uint128 public globalIssuance; + D18 public globalNetAssetValue; + mapping(uint16 centrifugeId => NetworkMetrics) public metrics; + + constructor(PoolId poolId_, ShareClassId scId_, IHub hub_) { + require(hub.shareClassManager().shareClassCount(poolId_) == 1, InvalidShareClassCount()); + + poolId = poolId_; + scId = scId_; + + hub = hub_; + shareClassManager = hub.shareClassManager(); + } + + //---------------------------------------------------------------------------------------------- + // Price updates + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc INAVHook + function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue_) external { + // TODO: check msg.sender + + NetworkMetrics storage networkMetrics = metrics[centrifugeId]; + uint128 issuance = shareClassManager.issuance(scId, centrifugeId); + + globalIssuance = globalIssuance + issuance - networkMetrics.issuance; + globalNetAssetValue = globalNetAssetValue + netAssetValue_ - networkMetrics.netAssetValue; + D18 price = globalNetAssetValue / d18(globalIssuance); + + networkMetrics.netAssetValue = netAssetValue_; + networkMetrics.issuance = issuance; + + hub.updateSharePrice(poolId, scId, price); + } + + //---------------------------------------------------------------------------------------------- + // Calculations + //---------------------------------------------------------------------------------------------- + + function navPoolPerShare() public view returns (D18) { + return globalNetAssetValue / d18(globalIssuance); + } +} From 8f8f60b227d233fa7751a23c045a9deab832852e Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 12:30:07 +0200 Subject: [PATCH 21/83] Asset/expense account shared across sc's --- src/common/types/AccountId.sol | 6 +++++- src/managers/NAVManager.sol | 33 +++++++++++++++++---------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/common/types/AccountId.sol b/src/common/types/AccountId.sol index 38843d0b0..355c50dd0 100644 --- a/src/common/types/AccountId.sol +++ b/src/common/types/AccountId.sol @@ -11,8 +11,12 @@ function neq(AccountId a, AccountId b) pure returns (bool) { return AccountId.unwrap(a) != AccountId.unwrap(b); } +function isNull(AccountId accountId) pure returns (bool) { + return AccountId.unwrap(accountId) == 0; +} + function withCentrifugeId(uint16 centrifugeId, uint16 index) pure returns (AccountId) { return AccountId.wrap((uint32(centrifugeId) << 16) | uint32(index)); } -using {raw, neq as !=} for AccountId global; +using {raw, neq as !=, isNull} for AccountId global; diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index d39ad693e..05d9f2dac 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -27,7 +27,6 @@ contract NAVManager is Auth, ISnapshotHook { error ExceedsMaxAccounts(); PoolId public immutable poolId; - ShareClassId public immutable scId; IHub public immutable hub; address public immutable holdings; @@ -35,10 +34,10 @@ contract NAVManager is Auth, ISnapshotHook { INAVHook public navHook; mapping(uint16 centrifugeId => uint16) public accountCounter; + mapping(uint16 centrifugeId => mapping(AssetId => AccountId)) public assetIdToAccountId; - constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { + constructor(PoolId poolId_, IHub hub_, address deployer) Auth(deployer) { poolId = poolId_; - scId = scId_; hub = hub_; holdings = address(hub.holdings()); @@ -68,15 +67,16 @@ contract NAVManager is Auth, ISnapshotHook { accountCounter[centrifugeId] = 5; } - function initializeHolding(AssetId assetId, IValuation valuation) external auth { + function initializeHolding(ShareClassId scId, AssetId assetId, IValuation valuation) external auth { uint16 centrifugeId = assetId.centrifugeId(); uint16 index = accountCounter[centrifugeId]; require(index > 0, NotInitialized()); require(index < type(uint16).max, ExceedsMaxAccounts()); - AccountId assetAccount = withCentrifugeId(centrifugeId, index); + AccountId assetAccount = assetIdToAccountId[centrifugeId][assetId]; + if (assetAccount.isNull()) assetAccount = withCentrifugeId(centrifugeId, index); + hub.createAccount(poolId, assetAccount, true); - // TOOD: should be adapted to asset account and holding per scId hub.initializeHolding( poolId, scId, @@ -91,15 +91,16 @@ contract NAVManager is Auth, ISnapshotHook { accountCounter[centrifugeId] = index + 1; } - function initializeLiability(AssetId assetId, IValuation valuation) external auth { + function initializeLiability(ShareClassId scId, AssetId assetId, IValuation valuation) external auth { uint16 centrifugeId = assetId.centrifugeId(); uint16 index = accountCounter[centrifugeId]; require(index > 0, NotInitialized()); require(index < type(uint16).max, ExceedsMaxAccounts()); - AccountId expenseAccount = withCentrifugeId(centrifugeId, index); + AccountId expenseAccount = assetIdToAccountId[centrifugeId][assetId]; + if (expenseAccount.isNull()) expenseAccount = withCentrifugeId(centrifugeId, index); + hub.createAccount(poolId, expenseAccount, true); - // TOOD: should be adapted to expense account and liability per scId hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount, liabilityAccount(centrifugeId)); accountCounter[centrifugeId] = index + 1; @@ -110,15 +111,15 @@ contract NAVManager is Auth, ISnapshotHook { //---------------------------------------------------------------------------------------------- /// @inheritdoc ISnapshotHook - function onSync(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId) external { - require(poolId == poolId_ && scId == scId_); + function onSync(PoolId poolId_, ShareClassId scId, uint16 centrifugeId) external { + require(poolId == poolId_); require(msg.sender == holdings, NotAuthorized()); D18 netAssetValue_ = netAssetValue(centrifugeId); navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); } - function updateHoldingValue(AssetId assetId) external { + function updateHoldingValue(ShareClassId scId, AssetId assetId) external { hub.updateHoldingValue(poolId, scId, assetId); } @@ -144,18 +145,18 @@ contract NAVManager is Auth, ISnapshotHook { //---------------------------------------------------------------------------------------------- function equityAccount(uint16 centrifugeId) public pure returns (AccountId) { - return equityAccount(centrifugeId); + return withCentrifugeId(centrifugeId, 1); } function liabilityAccount(uint16 centrifugeId) public pure returns (AccountId) { - return liabilityAccount(centrifugeId); + return withCentrifugeId(centrifugeId, 2); } function gainAccount(uint16 centrifugeId) public pure returns (AccountId) { - return gainAccount(centrifugeId); + return withCentrifugeId(centrifugeId, 3); } function lossAccount(uint16 centrifugeId) public pure returns (AccountId) { - return lossAccount(centrifugeId); + return withCentrifugeId(centrifugeId, 4); } } From b392f306ffa3d0f6593f39177d57e51e0486511b Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 13:21:42 +0200 Subject: [PATCH 22/83] Start adapting end to end tests --- src/managers/NAVManager.sol | 25 ++++++++++------ test/integration/EndToEnd.t.sol | 53 ++++++++++++++------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 05d9f2dac..814348b13 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -21,7 +21,6 @@ interface INAVHook { /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is Auth, ISnapshotHook { - error InvalidShareClassCount(); error AlreadyInitialized(); error NotInitialized(); error ExceedsMaxAccounts(); @@ -73,16 +72,16 @@ contract NAVManager is Auth, ISnapshotHook { require(index > 0, NotInitialized()); require(index < type(uint16).max, ExceedsMaxAccounts()); - AccountId assetAccount = assetIdToAccountId[centrifugeId][assetId]; - if (assetAccount.isNull()) assetAccount = withCentrifugeId(centrifugeId, index); + AccountId assetAccount_ = assetIdToAccountId[centrifugeId][assetId]; + if (assetAccount_.isNull()) assetAccount_ = withCentrifugeId(centrifugeId, index); - hub.createAccount(poolId, assetAccount, true); + hub.createAccount(poolId, assetAccount_, true); hub.initializeHolding( poolId, scId, assetId, valuation, - assetAccount, + assetAccount_, equityAccount(centrifugeId), gainAccount(centrifugeId), lossAccount(centrifugeId) @@ -97,11 +96,11 @@ contract NAVManager is Auth, ISnapshotHook { require(index > 0, NotInitialized()); require(index < type(uint16).max, ExceedsMaxAccounts()); - AccountId expenseAccount = assetIdToAccountId[centrifugeId][assetId]; - if (expenseAccount.isNull()) expenseAccount = withCentrifugeId(centrifugeId, index); + AccountId expenseAccount_ = assetIdToAccountId[centrifugeId][assetId]; + if (expenseAccount_.isNull()) expenseAccount_ = withCentrifugeId(centrifugeId, index); - hub.createAccount(poolId, expenseAccount, true); - hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount, liabilityAccount(centrifugeId)); + hub.createAccount(poolId, expenseAccount_, true); + hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount_, liabilityAccount(centrifugeId)); accountCounter[centrifugeId] = index + 1; } @@ -144,6 +143,14 @@ contract NAVManager is Auth, ISnapshotHook { // Helpers //---------------------------------------------------------------------------------------------- + function assetAccount(uint16 centrifugeId, AssetId assetId) public view returns (AccountId) { + return assetIdToAccountId[centrifugeId][assetId]; + } + + function expenseAccount(uint16 centrifugeId, AssetId assetId) public view returns (AccountId) { + return assetAccount(centrifugeId, assetId); + } + function equityAccount(uint16 centrifugeId) public pure returns (AccountId) { return withCentrifugeId(centrifugeId, 1); } diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index 5ac585b43..ec1c84e1a 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -39,11 +39,12 @@ import {UpdateContractMessageLib} from "src/spoke/libraries/UpdateContractMessag import {UpdateRestrictionMessageLib} from "src/hooks/libraries/UpdateRestrictionMessageLib.sol"; +import {NAVManager} from "src/managers/NAVManager.sol"; + import {FullDeployer} from "script/FullDeployer.s.sol"; import {MESSAGE_COST_ENV} from "script/CommonDeployer.s.sol"; import {MockValuation} from "test/common/mocks/MockValuation.sol"; -import {MockSnapshotHook} from "test/hooks/mocks/MockSnapshotHook.sol"; import {LocalAdapter} from "test/integration/adapters/LocalAdapter.sol"; import "forge-std/Test.sol"; @@ -84,7 +85,7 @@ contract EndToEndDeployment is Test { // Others IdentityValuation identityValuation; MockValuation valuation; - MockSnapshotHook snapshotHook; + NAVManager navManager; } struct CSpoke { @@ -125,11 +126,6 @@ contract EndToEndDeployment is Test { uint128 constant USDC_AMOUNT_1 = 1_000_000e6; // Measured in USDC: 1M of USDC - AccountId constant ASSET_ACCOUNT = AccountId.wrap(0x01); - AccountId constant EQUITY_ACCOUNT = AccountId.wrap(0x02); - AccountId constant LOSS_ACCOUNT = AccountId.wrap(0x03); - AccountId constant GAIN_ACCOUNT = AccountId.wrap(0x04); - AssetId USD_ID; PoolId POOL_A; ShareClassId SC_1; @@ -179,6 +175,11 @@ contract EndToEndDeployment is Test { // We not use the VM chain vm.chainId(0xDEAD); + // Initialize default values + USD_ID = deployA.USD_ID(); + POOL_A = deployA.hubRegistry().poolId(CENTRIFUGE_ID_A, 1); + SC_1 = deployA.shareClassManager().previewNextShareClassId(POOL_A); + h = CHub({ centrifugeId: CENTRIFUGE_ID_A, root: deployA.root(), @@ -191,14 +192,9 @@ contract EndToEndDeployment is Test { hub: deployA.hub(), identityValuation: deployA.identityValuation(), valuation: new MockValuation(deployA.hubRegistry()), - snapshotHook: new MockSnapshotHook() + navManager: new NAVManager(POOL_A, deployA.hub(), FM) }); - // Initialize default values - USD_ID = deployA.USD_ID(); - POOL_A = h.hubRegistry.poolId(CENTRIFUGE_ID_A, 1); - SC_1 = h.shareClassManager.previewNextShareClassId(POOL_A); - vm.label(address(adapterAToB), "AdapterAToB"); vm.label(address(adapterBToA), "AdapterBToA"); vm.label(address(h.hub), "Hub"); @@ -341,10 +337,7 @@ contract EndToEndFlows is EndToEndUtils { h.hub.setPoolMetadata(POOL_A, bytes("Testing pool")); h.hub.addShareClass(POOL_A, "Tokenized MMF", "MMF", bytes32("salt")); - h.hub.createAccount(POOL_A, ASSET_ACCOUNT, true); - h.hub.createAccount(POOL_A, EQUITY_ACCOUNT, false); - h.hub.createAccount(POOL_A, LOSS_ACCOUNT, false); - h.hub.createAccount(POOL_A, GAIN_ACCOUNT, false); + h.hub.updateHubManager(POOL_A, address(h.navManager), true); vm.stopPrank(); } @@ -360,16 +353,16 @@ contract EndToEndFlows is EndToEndUtils { h.hub.notifyPool{value: GAS}(POOL_A, s_.centrifugeId); h.hub.notifyShareClass{value: GAS}(POOL_A, SC_1, s_.centrifugeId, s_.redemptionRestrictionsHook.toBytes32()); - h.hub.initializeHolding( - POOL_A, SC_1, s_.usdcId, h.valuation, ASSET_ACCOUNT, EQUITY_ACCOUNT, GAIN_ACCOUNT, LOSS_ACCOUNT - ); + h.navManager.initializeNetwork(s_.centrifugeId); + h.navManager.initializeHolding(SC_1, s_.usdcId, h.valuation); + h.hub.setRequestManager{value: GAS}(POOL_A, SC_1, s_.usdcId, address(s.asyncRequestManager).toBytes32()); h.hub.updateBalanceSheetManager{value: GAS}( s_.centrifugeId, POOL_A, address(s.asyncRequestManager).toBytes32(), true ); h.hub.updateBalanceSheetManager{value: GAS}(s_.centrifugeId, POOL_A, address(s.syncManager).toBytes32(), true); h.hub.updateBalanceSheetManager{value: GAS}(s_.centrifugeId, POOL_A, BSM.toBytes32(), true); - h.hub.setSnapshotHook(POOL_A, h.snapshotHook); + h.hub.setSnapshotHook(POOL_A, h.navManager); vm.startPrank(BSM); s_.gateway.subsidizePool{value: DEFAULT_SUBSIDY}(POOL_A); @@ -539,10 +532,10 @@ contract EndToEndFlows is EndToEndUtils { assertEq(amount, USDC_AMOUNT_1, "expected amount"); assertEq(value, assetToPool(USDC_AMOUNT_1), "expected value"); - assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), nonZeroPrices ? 1 : 2, "expected snapshots"); + // assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), nonZeroPrices ? 1 : 2, "expected snapshots"); - checkAccountValue(ASSET_ACCOUNT, assetToPool(USDC_AMOUNT_1), true); - checkAccountValue(EQUITY_ACCOUNT, assetToPool(USDC_AMOUNT_1), true); + checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1), true); + checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1), true); } function _testUpdateAccountingAfterRedeem(bool sameChain, bool afterAsyncDeposit) public { @@ -557,10 +550,10 @@ contract EndToEndFlows is EndToEndUtils { assertEq(amount, 0, "expected amount"); assertEq(value, assetToPool(0), "expected value"); - assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 2, "expected snapshots"); + // assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 2, "expected snapshots"); - checkAccountValue(ASSET_ACCOUNT, assetToPool(0), true); - checkAccountValue(EQUITY_ACCOUNT, assetToPool(0), true); + checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(0), true); + checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(0), true); } } @@ -605,10 +598,10 @@ contract EndToEndUseCases is EndToEndFlows { assertEq(amount, USDC_AMOUNT_1 / 5); assertEq(value, assetToPool(USDC_AMOUNT_1 / 5)); - assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 1); + // assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 1); - checkAccountValue(ASSET_ACCOUNT, assetToPool(USDC_AMOUNT_1 / 5), true); - checkAccountValue(EQUITY_ACCOUNT, assetToPool(USDC_AMOUNT_1 / 5), true); + checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1 / 5), true); + checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1 / 5), true); } /// forge-config: default.isolate = true From 18243e9e39011fb968cefa0cf51206fcc5eaddfd Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 13:33:48 +0200 Subject: [PATCH 23/83] Start to integrate price manager --- src/managers/NAVManager.sol | 4 ++++ src/managers/SimplePriceManager.sol | 6 +++--- test/integration/EndToEnd.t.sol | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 814348b13..70f3ac682 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -14,6 +14,8 @@ import {ISnapshotHook} from "src/common/interfaces/ISnapshotHook.sol"; import {IHub} from "src/hub/interfaces/IHub.sol"; import {IAccounting} from "src/hub/interfaces/IAccounting.sol"; +import { console } from "forge-std/console.sol"; + interface INAVHook { /// @notice Callback when there is a new net asset value (NAV) on a specific network. function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external; @@ -24,6 +26,7 @@ contract NAVManager is Auth, ISnapshotHook { error AlreadyInitialized(); error NotInitialized(); error ExceedsMaxAccounts(); + error InvalidNAVHook(); PoolId public immutable poolId; @@ -113,6 +116,7 @@ contract NAVManager is Auth, ISnapshotHook { function onSync(PoolId poolId_, ShareClassId scId, uint16 centrifugeId) external { require(poolId == poolId_); require(msg.sender == holdings, NotAuthorized()); + require(address(navHook) != address(0), InvalidNAVHook()); D18 netAssetValue_ = netAssetValue(centrifugeId); navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index bcd14635c..8d844ee7f 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -31,13 +31,13 @@ contract SimplePriceManager is INAVHook { mapping(uint16 centrifugeId => NetworkMetrics) public metrics; constructor(PoolId poolId_, ShareClassId scId_, IHub hub_) { - require(hub.shareClassManager().shareClassCount(poolId_) == 1, InvalidShareClassCount()); - poolId = poolId_; scId = scId_; hub = hub_; - shareClassManager = hub.shareClassManager(); + shareClassManager = hub_.shareClassManager(); + + require(shareClassManager.shareClassCount(poolId_) == 1, InvalidShareClassCount()); } //---------------------------------------------------------------------------------------------- diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index ec1c84e1a..8be7352c8 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -40,6 +40,7 @@ import {UpdateContractMessageLib} from "src/spoke/libraries/UpdateContractMessag import {UpdateRestrictionMessageLib} from "src/hooks/libraries/UpdateRestrictionMessageLib.sol"; import {NAVManager} from "src/managers/NAVManager.sol"; +import {SimplePriceManager} from "src/managers/SimplePriceManager.sol"; import {FullDeployer} from "script/FullDeployer.s.sol"; import {MESSAGE_COST_ENV} from "script/CommonDeployer.s.sol"; @@ -339,6 +340,8 @@ contract EndToEndFlows is EndToEndUtils { h.hub.updateHubManager(POOL_A, address(h.navManager), true); + h.navManager.setNAVHook(new SimplePriceManager(POOL_A, SC_1, h.hub)); + vm.stopPrank(); } From 4b8345fedae9ff86cd25cec584f552216a83296a Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 13:42:11 +0200 Subject: [PATCH 24/83] Fix price with zero issuance --- src/managers/SimplePriceManager.sol | 4 +++- test/integration/EndToEnd.t.sol | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 8d844ee7f..de342709a 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -51,9 +51,11 @@ contract SimplePriceManager is INAVHook { NetworkMetrics storage networkMetrics = metrics[centrifugeId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); + globalIssuance = globalIssuance + issuance - networkMetrics.issuance; globalNetAssetValue = globalNetAssetValue + netAssetValue_ - networkMetrics.netAssetValue; - D18 price = globalNetAssetValue / d18(globalIssuance); + + D18 price = globalIssuance == 0 ? d18(1, 1) : globalNetAssetValue / d18(globalIssuance); networkMetrics.netAssetValue = netAssetValue_; networkMetrics.issuance = issuance; diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index 8be7352c8..32bab2f09 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -338,9 +338,11 @@ contract EndToEndFlows is EndToEndUtils { h.hub.setPoolMetadata(POOL_A, bytes("Testing pool")); h.hub.addShareClass(POOL_A, "Tokenized MMF", "MMF", bytes32("salt")); + SimplePriceManager priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub); + h.navManager.setNAVHook(priceManager); + h.hub.updateHubManager(POOL_A, address(h.navManager), true); - - h.navManager.setNAVHook(new SimplePriceManager(POOL_A, SC_1, h.hub)); + h.hub.updateHubManager(POOL_A, address(priceManager), true); vm.stopPrank(); } @@ -601,7 +603,9 @@ contract EndToEndUseCases is EndToEndFlows { assertEq(amount, USDC_AMOUNT_1 / 5); assertEq(value, assetToPool(USDC_AMOUNT_1 / 5)); - // assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 1); + (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); + assertEq(issuance, 0); + assertEq(poolPerShare.raw(), d18(1, 1).raw()); checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1 / 5), true); checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1 / 5), true); From e49444637855e7c8fde6193d958d710326ea2305 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 13:54:42 +0200 Subject: [PATCH 25/83] Fix tests --- src/managers/NAVManager.sol | 10 ++++++++-- src/managers/SimplePriceManager.sol | 9 --------- test/integration/EndToEnd.t.sol | 19 ++++++++++++------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 70f3ac682..345255540 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -76,7 +76,10 @@ contract NAVManager is Auth, ISnapshotHook { require(index < type(uint16).max, ExceedsMaxAccounts()); AccountId assetAccount_ = assetIdToAccountId[centrifugeId][assetId]; - if (assetAccount_.isNull()) assetAccount_ = withCentrifugeId(centrifugeId, index); + if (assetAccount_.isNull()) { + assetAccount_ = withCentrifugeId(centrifugeId, index); + assetIdToAccountId[centrifugeId][assetId] = assetAccount_; + } hub.createAccount(poolId, assetAccount_, true); hub.initializeHolding( @@ -100,7 +103,10 @@ contract NAVManager is Auth, ISnapshotHook { require(index < type(uint16).max, ExceedsMaxAccounts()); AccountId expenseAccount_ = assetIdToAccountId[centrifugeId][assetId]; - if (expenseAccount_.isNull()) expenseAccount_ = withCentrifugeId(centrifugeId, index); + if (expenseAccount_.isNull()) { + expenseAccount_ = withCentrifugeId(centrifugeId, index); + assetIdToAccountId[centrifugeId][assetId] = expenseAccount_; + } hub.createAccount(poolId, expenseAccount_, true); hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount_, liabilityAccount(centrifugeId)); diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index de342709a..76acab797 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -51,7 +51,6 @@ contract SimplePriceManager is INAVHook { NetworkMetrics storage networkMetrics = metrics[centrifugeId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - globalIssuance = globalIssuance + issuance - networkMetrics.issuance; globalNetAssetValue = globalNetAssetValue + netAssetValue_ - networkMetrics.netAssetValue; @@ -62,12 +61,4 @@ contract SimplePriceManager is INAVHook { hub.updateSharePrice(poolId, scId, price); } - - //---------------------------------------------------------------------------------------------- - // Calculations - //---------------------------------------------------------------------------------------------- - - function navPoolPerShare() public view returns (D18) { - return globalNetAssetValue / d18(globalIssuance); - } } diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index 32bab2f09..f45dcceb3 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -537,10 +537,13 @@ contract EndToEndFlows is EndToEndUtils { assertEq(amount, USDC_AMOUNT_1, "expected amount"); assertEq(value, assetToPool(USDC_AMOUNT_1), "expected value"); - // assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), nonZeroPrices ? 1 : 2, "expected snapshots"); - checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1), true); checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1), true); + + // (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); + // assertEq(issuance, ); + // assertEq(poolPerShare.raw(), d18(1, 1).raw()); + } function _testUpdateAccountingAfterRedeem(bool sameChain, bool afterAsyncDeposit) public { @@ -555,10 +558,12 @@ contract EndToEndFlows is EndToEndUtils { assertEq(amount, 0, "expected amount"); assertEq(value, assetToPool(0), "expected value"); - // assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 2, "expected snapshots"); - checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(0), true); checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(0), true); + + // (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); + // assertEq(issuance, 0); + // assertEq(poolPerShare.raw(), d18(1, 1).raw()); } } @@ -603,12 +608,12 @@ contract EndToEndUseCases is EndToEndFlows { assertEq(amount, USDC_AMOUNT_1 / 5); assertEq(value, assetToPool(USDC_AMOUNT_1 / 5)); + checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1 / 5), true); + checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1 / 5), true); + (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); assertEq(issuance, 0); assertEq(poolPerShare.raw(), d18(1, 1).raw()); - - checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1 / 5), true); - checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1 / 5), true); } /// forge-config: default.isolate = true From 2f2446154d12968d9d110a2a874ce0e07a06ba28 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 13:55:29 +0200 Subject: [PATCH 26/83] Undo --- src/hub/Accounting.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hub/Accounting.sol b/src/hub/Accounting.sol index f50350652..618f2f6a2 100644 --- a/src/hub/Accounting.sol +++ b/src/hub/Accounting.sol @@ -10,8 +10,8 @@ import {AccountId} from "src/common/types/AccountId.sol"; import {IAccounting, JournalEntry} from "src/hub/interfaces/IAccounting.sol"; /// @notice In a transaction there can be multiple journal entries for different pools, -/// which can be interleaved. We want entries for the same pool to share the same journal ID. -/// So we're keeping a journal ID per pool in transient storage. +/// which can be interleaved. We want entries for the same pool to share the same journal ID. +/// So we're keeping a journal ID per pool in transient storage. library TransientJournal { function journalId(PoolId poolId) internal view returns (uint256) { return TransientStorageLib.tloadUint256(keccak256(abi.encode("journalId", poolId))); From 128829dc7b4333bb64f4b5b4fcbd09291b66f42c Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 14:21:43 +0200 Subject: [PATCH 27/83] Fix warnings --- src/managers/SimplePriceManager.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 76acab797..518376acb 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -30,6 +30,7 @@ contract SimplePriceManager is INAVHook { D18 public globalNetAssetValue; mapping(uint16 centrifugeId => NetworkMetrics) public metrics; + constructor(PoolId poolId_, ShareClassId scId_, IHub hub_) { poolId = poolId_; scId = scId_; @@ -46,6 +47,8 @@ contract SimplePriceManager is INAVHook { /// @inheritdoc INAVHook function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue_) external { + require(poolId == poolId_); + require(scId == scId_); // TODO: check msg.sender NetworkMetrics storage networkMetrics = metrics[centrifugeId]; From 8245f746bffe86bc2044131c3bd58e1c95a93cbc Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 14:21:56 +0200 Subject: [PATCH 28/83] Format --- src/managers/NAVManager.sol | 2 +- src/managers/SimplePriceManager.sol | 3 +-- test/integration/EndToEnd.t.sol | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 345255540..d35ef9647 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -14,7 +14,7 @@ import {ISnapshotHook} from "src/common/interfaces/ISnapshotHook.sol"; import {IHub} from "src/hub/interfaces/IHub.sol"; import {IAccounting} from "src/hub/interfaces/IAccounting.sol"; -import { console } from "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; interface INAVHook { /// @notice Callback when there is a new net asset value (NAV) on a specific network. diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 518376acb..c0826d8d3 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -30,7 +30,6 @@ contract SimplePriceManager is INAVHook { D18 public globalNetAssetValue; mapping(uint16 centrifugeId => NetworkMetrics) public metrics; - constructor(PoolId poolId_, ShareClassId scId_, IHub hub_) { poolId = poolId_; scId = scId_; @@ -56,7 +55,7 @@ contract SimplePriceManager is INAVHook { globalIssuance = globalIssuance + issuance - networkMetrics.issuance; globalNetAssetValue = globalNetAssetValue + netAssetValue_ - networkMetrics.netAssetValue; - + D18 price = globalIssuance == 0 ? d18(1, 1) : globalNetAssetValue / d18(globalIssuance); networkMetrics.netAssetValue = netAssetValue_; diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index f45dcceb3..f1637fa5f 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -340,7 +340,7 @@ contract EndToEndFlows is EndToEndUtils { SimplePriceManager priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub); h.navManager.setNAVHook(priceManager); - + h.hub.updateHubManager(POOL_A, address(h.navManager), true); h.hub.updateHubManager(POOL_A, address(priceManager), true); @@ -543,7 +543,6 @@ contract EndToEndFlows is EndToEndUtils { // (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); // assertEq(issuance, ); // assertEq(poolPerShare.raw(), d18(1, 1).raw()); - } function _testUpdateAccountingAfterRedeem(bool sameChain, bool afterAsyncDeposit) public { From 96f66c2bd27b35ca1066fc1edd1aa54c6066433c Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 29 Jun 2025 14:22:43 +0200 Subject: [PATCH 29/83] Fix import order --- src/managers/NAVManager.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index d35ef9647..735ff0336 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -6,16 +6,14 @@ import {D18, d18} from "src/misc/types/D18.sol"; import {PoolId} from "src/common/types/PoolId.sol"; import {AssetId} from "src/common/types/AssetId.sol"; -import {AccountId, withCentrifugeId} from "src/common/types/AccountId.sol"; import {ShareClassId} from "src/common/types/ShareClassId.sol"; import {IValuation} from "src/common/interfaces/IValuation.sol"; import {ISnapshotHook} from "src/common/interfaces/ISnapshotHook.sol"; +import {AccountId, withCentrifugeId} from "src/common/types/AccountId.sol"; import {IHub} from "src/hub/interfaces/IHub.sol"; import {IAccounting} from "src/hub/interfaces/IAccounting.sol"; -import {console} from "forge-std/console.sol"; - interface INAVHook { /// @notice Callback when there is a new net asset value (NAV) on a specific network. function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external; From 4c4fc1b144712514aa717de1b6eb12ecb4686ed0 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 6 Jul 2025 14:34:58 +0200 Subject: [PATCH 30/83] Update hub diagram --- docs/architecture/hub.puml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/architecture/hub.puml b/docs/architecture/hub.puml index 9f943e3ff..aa3e6c6a0 100644 --- a/docs/architecture/hub.puml +++ b/docs/architecture/hub.puml @@ -12,6 +12,12 @@ package common { class Gateway } +package managers { + class NAVManager + class SimplePriceManager + interface INAVHook +} + class Holdings class HubRegistry class ShareClassManager @@ -34,7 +40,11 @@ Hub --> Accounting Hub -up-> MessageDispatcher Hub -up-> Gateway Holdings --> IValuation -Holdings --> ISnapshotHook +Holdings -|> ISnapshotHook + +NAVManager -|> ISnapshotHook +NAVManager -down-|> INAVHook +SimplePriceManager -up-|> INAVHook Holdings --> HubRegistry ShareClassManager --> HubRegistry From dbfc3e12eb2adf7120b4feb5479d4fd3880f9e0a Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 6 Jul 2025 15:11:21 +0200 Subject: [PATCH 31/83] Make it compile --- src/managers/SimplePriceManager.sol | 33 +++++++++++++++++++++++------ test/integration/EndToEnd.t.sol | 2 +- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index c0826d8d3..220bf355e 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {Auth} from "src/misc/Auth.sol"; import {D18, d18} from "src/misc/types/D18.sol"; +import {IMulticall} from "src/misc/interfaces/IMulticall.sol"; import {PoolId} from "src/common/types/PoolId.sol"; import {ShareClassId} from "src/common/types/ShareClassId.sol"; +import {MAX_MESSAGE_COST} from "src/common/interfaces/IGasService.sol"; import {IHub} from "src/hub/interfaces/IHub.sol"; import {IShareClassManager} from "src/hub/interfaces/IShareClassManager.sol"; @@ -17,7 +20,7 @@ struct NetworkMetrics { } /// @notice Share price calculation manager for single share class pools. -contract SimplePriceManager is INAVHook { +contract SimplePriceManager is Auth, INAVHook { error InvalidShareClassCount(); PoolId public immutable poolId; @@ -26,11 +29,12 @@ contract SimplePriceManager is INAVHook { IHub public immutable hub; IShareClassManager public immutable shareClassManager; + uint16[] public networks; uint128 public globalIssuance; D18 public globalNetAssetValue; mapping(uint16 centrifugeId => NetworkMetrics) public metrics; - constructor(PoolId poolId_, ShareClassId scId_, IHub hub_) { + constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { poolId = poolId_; scId = scId_; @@ -40,12 +44,21 @@ contract SimplePriceManager is INAVHook { require(shareClassManager.shareClassCount(poolId_) == 1, InvalidShareClassCount()); } + //---------------------------------------------------------------------------------------------- + // Network management + //---------------------------------------------------------------------------------------------- + + /// @dev Ensure the number of network updates can fit in a single block + function setNetworks(uint16[] calldata centrifugeIds) external auth { + networks = centrifugeIds; + } + //---------------------------------------------------------------------------------------------- // Price updates //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVHook - function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue_) external { + function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external { require(poolId == poolId_); require(scId == scId_); // TODO: check msg.sender @@ -54,13 +67,21 @@ contract SimplePriceManager is INAVHook { uint128 issuance = shareClassManager.issuance(scId, centrifugeId); globalIssuance = globalIssuance + issuance - networkMetrics.issuance; - globalNetAssetValue = globalNetAssetValue + netAssetValue_ - networkMetrics.netAssetValue; + globalNetAssetValue = globalNetAssetValue + netAssetValue - networkMetrics.netAssetValue; D18 price = globalIssuance == 0 ? d18(1, 1) : globalNetAssetValue / d18(globalIssuance); - networkMetrics.netAssetValue = netAssetValue_; + networkMetrics.netAssetValue = netAssetValue; networkMetrics.issuance = issuance; - hub.updateSharePrice(poolId, scId, price); + uint256 networkCount = networks.length; + bytes[] memory cs = new bytes[](networkCount + 1); + cs[0] = abi.encodeWithSelector(hub.updateSharePrice.selector, poolId, scId, price); + + for (uint256 i; i < networkCount; i++) { + cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, centrifugeId); + } + + IMulticall(address(hub)).multicall{value: MAX_MESSAGE_COST * (cs.length)}(cs); } } diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index d19d79194..fef1e8dfa 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -353,7 +353,7 @@ contract EndToEndFlows is EndToEndUtils { h.hub.setPoolMetadata(POOL_A, bytes("Testing pool")); h.hub.addShareClass(POOL_A, "Tokenized MMF", "MMF", bytes32("salt")); - SimplePriceManager priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub); + SimplePriceManager priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub, address(this)); h.navManager.setNAVHook(priceManager); h.hub.updateHubManager(POOL_A, address(h.navManager), true); From 421e4530ad0ca62cec0801af602f2611eb8d1cc9 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Tue, 22 Jul 2025 13:33:11 +0200 Subject: [PATCH 32/83] Fix end to end test compilation --- test/integration/EndToEnd.t.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index 4278d08ab..6f9fcfc77 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -660,7 +660,7 @@ contract EndToEndUseCases is EndToEndFlows { h.hub.updateBalanceSheetManager{value: GAS}(s.centrifugeId, POOL_A, BSM.toBytes32(), true); h.hub.updateSharePrice(POOL_A, SC_1, ZERO_PRICE); h.hub.notifySharePrice{value: GAS}(POOL_A, SC_1, s.centrifugeId); - h.hub.setSnapshotHook(POOL_A, h.snapshotHook); + h.hub.setSnapshotHook(POOL_A, h.navManager); // Each message will return half of the gas wasted adapterBToA.setRefundedValue(h.gasService.updateShares() / 2); @@ -684,8 +684,6 @@ contract EndToEndUseCases is EndToEndFlows { s.balanceSheet.submitQueuedShares(POOL_A, SC_1, EXTRA_GAS); assertEq(address(s.balanceSheet.escrow(POOL_A)).balance, h.gasService.updateShares() / 2); assertEq(address(s.gateway).balance, 0); - - assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 3, "3 UpdateShares messages received"); } /// forge-config: default.isolate = true From 8c2c95f52d583aa186dfbad370159f0856773cf8 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 27 Jul 2025 20:29:05 +0200 Subject: [PATCH 33/83] Add onTransfer --- src/common/MessageDispatcher.sol | 2 +- src/common/MessageProcessor.sol | 8 +++++++- src/common/interfaces/IGatewayHandlers.sol | 3 ++- src/common/interfaces/ISnapshotHook.sol | 3 +++ src/hub/Hub.sol | 13 ++++++++++--- src/hub/interfaces/IHoldings.sol | 3 +++ src/hub/interfaces/IHub.sol | 7 ++++++- src/managers/NAVManager.sol | 4 ++++ test/hooks/mocks/MockSnapshotHook.sol | 4 ++++ test/hub/unit/Hub.t.sol | 2 +- test/integration/ThreeChainEndToEnd.t.sol | 2 +- 11 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/common/MessageDispatcher.sol b/src/common/MessageDispatcher.sol index 7b253a236..574bbef3b 100644 --- a/src/common/MessageDispatcher.sol +++ b/src/common/MessageDispatcher.sol @@ -370,7 +370,7 @@ contract MessageDispatcher is Auth, IMessageDispatcher { uint128 remoteExtraGasLimit ) external auth { if (poolId.centrifugeId() == localCentrifugeId) { - hub.initiateTransferShares(targetCentrifugeId, poolId, scId, receiver, amount, remoteExtraGasLimit); + hub.initiateTransferShares(localCentrifugeId, targetCentrifugeId, poolId, scId, receiver, amount, remoteExtraGasLimit); } else { gateway.send( poolId.centrifugeId(), diff --git a/src/common/MessageProcessor.sol b/src/common/MessageProcessor.sol index 80d715ef0..a8076a42a 100644 --- a/src/common/MessageProcessor.sol +++ b/src/common/MessageProcessor.sol @@ -114,7 +114,13 @@ contract MessageProcessor is Auth, IMessageProcessor { } else if (kind == MessageType.InitiateTransferShares) { MessageLib.InitiateTransferShares memory m = MessageLib.deserializeInitiateTransferShares(message); hub.initiateTransferShares( - m.centrifugeId, PoolId.wrap(m.poolId), ShareClassId.wrap(m.scId), m.receiver, m.amount, m.extraGasLimit + sourceCentrifugeId, + m.centrifugeId, + PoolId.wrap(m.poolId), + ShareClassId.wrap(m.scId), + m.receiver, + m.amount, + m.extraGasLimit ); } else if (kind == MessageType.ExecuteTransferShares) { MessageLib.ExecuteTransferShares memory m = MessageLib.deserializeExecuteTransferShares(message); diff --git a/src/common/interfaces/IGatewayHandlers.sol b/src/common/interfaces/IGatewayHandlers.sol index c9c2445e7..f244b8513 100644 --- a/src/common/interfaces/IGatewayHandlers.sol +++ b/src/common/interfaces/IGatewayHandlers.sol @@ -39,7 +39,8 @@ interface IHubGatewayHandler { /// @notice Forward an initiated share transfer to the destination chain. function initiateTransferShares( - uint16 centrifugeId, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, PoolId poolId, ShareClassId scId, bytes32 receiver, diff --git a/src/common/interfaces/ISnapshotHook.sol b/src/common/interfaces/ISnapshotHook.sol index d4bd60dc3..e9005ce1b 100644 --- a/src/common/interfaces/ISnapshotHook.sol +++ b/src/common/interfaces/ISnapshotHook.sol @@ -17,4 +17,7 @@ import {ShareClassId} from "../types/ShareClassId.sol"; interface ISnapshotHook { /// @notice Callback when there is a sync snapshot. function onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external; + + /// @notice Callback when there is a cross-chain transfer leading to issuance updates + function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external; } diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 84ca09030..5e2d1d4f6 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -708,7 +708,8 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar /// @inheritdoc IHubGatewayHandler function initiateTransferShares( - uint16 centrifugeId, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, PoolId poolId, ShareClassId scId, bytes32 receiver, @@ -717,8 +718,14 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar ) external { _auth(); - emit ForwardTransferShares(centrifugeId, poolId, scId, receiver, amount); - sender.sendExecuteTransferShares(centrifugeId, poolId, scId, receiver, amount, extraGasLimit); + shareClassManager.updateShares(fromCentrifugeId, poolId, scId, amount, false); + shareClassManager.updateShares(toCentrifugeId, poolId, scId, amount, true); + + ISnapshotHook hook = holdings.snapshotHook(poolId); + if (address(hook) != address(0)) hook.onTransfer(poolId, scId, fromCentrifugeId, toCentrifugeId); + + emit ForwardTransferShares(fromCentrifugeId, toCentrifugeId, poolId, scId, receiver, amount); + sender.sendExecuteTransferShares(toCentrifugeId, poolId, scId, receiver, amount, extraGasLimit); } //---------------------------------------------------------------------------------------------- diff --git a/src/hub/interfaces/IHoldings.sol b/src/hub/interfaces/IHoldings.sol index e559f18b2..a5499b0cd 100644 --- a/src/hub/interfaces/IHoldings.sol +++ b/src/hub/interfaces/IHoldings.sol @@ -153,6 +153,9 @@ interface IHoldings { function setSnapshotHook(PoolId poolId, ISnapshotHook hook) external; + /// @notice Returns the snapshot hook for the given pool. + function snapshotHook(PoolId poolId) external view returns (ISnapshotHook); + /// @notice Returns the value of this holding. function value(PoolId poolId, ShareClassId scId, AssetId assetId) external view returns (uint128 value); diff --git a/src/hub/interfaces/IHub.sol b/src/hub/interfaces/IHub.sol index c2db3bae1..7685584e4 100644 --- a/src/hub/interfaces/IHub.sol +++ b/src/hub/interfaces/IHub.sol @@ -58,7 +58,12 @@ interface IHub { uint16 indexed centrifugeId, PoolId indexed poolId, ShareClassId scId, uint64 maxPriceAge ); event ForwardTransferShares( - uint16 indexed centrifugeId, PoolId indexed poolId, ShareClassId scId, bytes32 receiver, uint128 amount + uint16 indexed fromCentrifugeId, + uint16 indexed toCentrifugeId, + PoolId indexed poolId, + ShareClassId scId, + bytes32 receiver, + uint128 amount ); /// @notice Emitted when a call to `file()` was performed. diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 55e69bcfe..4695d04b3 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -126,6 +126,10 @@ contract NAVManager is Auth, ISnapshotHook { navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); } + function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external { + // TODO + } + function updateHoldingValue(ShareClassId scId, AssetId assetId) external { hub.updateHoldingValue(poolId, scId, assetId); } diff --git a/test/hooks/mocks/MockSnapshotHook.sol b/test/hooks/mocks/MockSnapshotHook.sol index c8ac836d9..c4ec10ca0 100644 --- a/test/hooks/mocks/MockSnapshotHook.sol +++ b/test/hooks/mocks/MockSnapshotHook.sol @@ -11,4 +11,8 @@ contract MockSnapshotHook is ISnapshotHook { function onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external { synced[poolId][scId][centrifugeId]++; } + + function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external { + // TODO + } } diff --git a/test/hub/unit/Hub.t.sol b/test/hub/unit/Hub.t.sol index 6afe58521..7b407f79d 100644 --- a/test/hub/unit/Hub.t.sol +++ b/test/hub/unit/Hub.t.sol @@ -78,7 +78,7 @@ contract TestMainMethodsChecks is TestCommon { hub.updateShares(CHAIN_A, PoolId.wrap(0), ShareClassId.wrap(0), 0, true, true, 0); vm.expectRevert(IAuth.NotAuthorized.selector); - hub.initiateTransferShares(CHAIN_A, PoolId.wrap(0), ShareClassId.wrap(0), bytes32(""), 0, 0); + hub.initiateTransferShares(CHAIN_A, CHAIN_A, PoolId.wrap(0), ShareClassId.wrap(0), bytes32(""), 0, 0); vm.stopPrank(); } diff --git a/test/integration/ThreeChainEndToEnd.t.sol b/test/integration/ThreeChainEndToEnd.t.sol index 23f25451b..5400890ec 100644 --- a/test/integration/ThreeChainEndToEnd.t.sol +++ b/test/integration/ThreeChainEndToEnd.t.sol @@ -121,7 +121,7 @@ contract ThreeChainEndToEndDeployment is EndToEndFlows { // B: Initiate transfer of shares vm.expectEmit(); emit ISpoke.InitiateTransferShares(sC.centrifugeId, POOL_A, SC_1, INVESTOR_A, INVESTOR_A.toBytes32(), amount); - emit IHub.ForwardTransferShares(sC.centrifugeId, POOL_A, SC_1, INVESTOR_A.toBytes32(), amount); + emit IHub.ForwardTransferShares(sB.centrifugeId, sC.centrifugeId, POOL_A, SC_1, INVESTOR_A.toBytes32(), amount); // If hub is not source, then message will be pending as unpaid on hub until repaid if (direction != CrossChainDirection.FromHub) { From 5fa5b035eb60a38fe45556315de33d9b743b9b10 Mon Sep 17 00:00:00 2001 From: Jeroen Offerijns Date: Sun, 3 Aug 2025 15:33:38 +0200 Subject: [PATCH 34/83] Formatting --- src/common/MessageDispatcher.sol | 4 +++- src/managers/NAVManager.sol | 2 +- src/managers/OnOfframpManager.sol | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/common/MessageDispatcher.sol b/src/common/MessageDispatcher.sol index 574bbef3b..c7eeb6b6b 100644 --- a/src/common/MessageDispatcher.sol +++ b/src/common/MessageDispatcher.sol @@ -370,7 +370,9 @@ contract MessageDispatcher is Auth, IMessageDispatcher { uint128 remoteExtraGasLimit ) external auth { if (poolId.centrifugeId() == localCentrifugeId) { - hub.initiateTransferShares(localCentrifugeId, targetCentrifugeId, poolId, scId, receiver, amount, remoteExtraGasLimit); + hub.initiateTransferShares( + localCentrifugeId, targetCentrifugeId, poolId, scId, receiver, amount, remoteExtraGasLimit + ); } else { gateway.send( poolId.centrifugeId(), diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 4695d04b3..1baffb947 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -126,7 +126,7 @@ contract NAVManager is Auth, ISnapshotHook { navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); } - function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external { + function onTransfer(PoolId poolId_, ShareClassId scId_, uint16 fromCentrifugeId, uint16 toCentrifugeId) external { // TODO } diff --git a/src/managers/OnOfframpManager.sol b/src/managers/OnOfframpManager.sol index a825ab5ae..2b526fd42 100644 --- a/src/managers/OnOfframpManager.sol +++ b/src/managers/OnOfframpManager.sol @@ -35,10 +35,10 @@ contract OnOfframpManager is IOnOfframpManager { mapping(address relayer => bool) public relayer; mapping(address asset => mapping(address receiver => bool isEnabled)) public offramp; - constructor(PoolId poolId_, ShareClassId scId_, address spoke_, IBalanceSheet balanceSheet_) { + constructor(PoolId poolId_, ShareClassId scId_, address contractUpdater_, IBalanceSheet balanceSheet_) { poolId = poolId_; scId = scId_; - contractUpdater = spoke_; + contractUpdater = contractUpdater_; balanceSheet = balanceSheet_; } From 4d4064f1e2b6dfb22c11cc3f7ff53f35124c9548 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:42:50 +0200 Subject: [PATCH 35/83] interfaces --- src/managers/NAVManager.sol | 30 ++++- src/managers/SimplePriceManager.sol | 12 +- src/managers/interfaces/INAVManager.sol | 110 ++++++++++++++++++ .../interfaces/INAVManagerFactory.sol | 15 +++ .../interfaces/ISimplePriceManager.sol | 24 ++++ test/integration/EndToEnd.t.sol | 3 +- 6 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 src/managers/interfaces/INAVManager.sol create mode 100644 src/managers/interfaces/INAVManagerFactory.sol create mode 100644 src/managers/interfaces/ISimplePriceManager.sol diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 1baffb947..b8fb40195 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -4,23 +4,21 @@ pragma solidity 0.8.28; import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; +import {INAVManagerFactory} from "./interfaces/INAVManagerFactory.sol"; +import {INAVManager, INAVHook} from "./interfaces/INAVManager.sol"; import {PoolId} from "../common/types/PoolId.sol"; import {AssetId} from "../common/types/AssetId.sol"; import {ShareClassId} from "../common/types/ShareClassId.sol"; import {IValuation} from "../common/interfaces/IValuation.sol"; import {ISnapshotHook} from "../common/interfaces/ISnapshotHook.sol"; +import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; import {AccountId, withCentrifugeId} from "../common/types/AccountId.sol"; import {IHub} from "../hub/interfaces/IHub.sol"; import {IAccounting} from "../hub/interfaces/IAccounting.sol"; -interface INAVHook { - /// @notice Callback when there is a new net asset value (NAV) on a specific network. - function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external; -} - /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. -contract NAVManager is Auth, ISnapshotHook { +contract NAVManager is Auth, INAVManager { error AlreadyInitialized(); error NotInitialized(); error ExceedsMaxAccounts(); @@ -179,3 +177,23 @@ contract NAVManager is Auth, ISnapshotHook { return withCentrifugeId(centrifugeId, 4); } } + +contract NavManagerFactory is INAVManagerFactory { + address public immutable contractUpdater; + IHub public immutable hub; + + constructor(address contractUpdater_, IHub hub_) { + contractUpdater = contractUpdater_; + hub = hub_; + } + + /// @inheritdoc INAVManagerFactory + function newManager(PoolId poolId) external returns (INAVManager) { + require(hub.hubRegistry().exists(poolId), InvalidPoolId()); + + NAVManager manager = new NAVManager{salt: bytes32(uint256(poolId.raw()))}(poolId, hub, contractUpdater); + + emit DeployNavManager(poolId, address(manager)); + return INAVManager(manager); + } +} diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 558a3a54a..cf339bc7a 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {INAVHook} from "./NAVManager.sol"; +import {INAVHook} from "./interfaces/INavManager.sol"; +import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; @@ -14,15 +15,8 @@ import {MAX_MESSAGE_COST} from "../common/interfaces/IGasService.sol"; import {IHub} from "../hub/interfaces/IHub.sol"; import {IShareClassManager} from "../hub/interfaces/IShareClassManager.sol"; -struct NetworkMetrics { - D18 netAssetValue; - uint128 issuance; -} - /// @notice Share price calculation manager for single share class pools. -contract SimplePriceManager is Auth, INAVHook { - error InvalidShareClassCount(); - +contract SimplePriceManager is Auth, ISimplePriceManager { PoolId public immutable poolId; ShareClassId public immutable scId; diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol new file mode 100644 index 000000000..49fa1ba45 --- /dev/null +++ b/src/managers/interfaces/INAVManager.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +import {ISnapshotHook} from "../../common/interfaces/ISnapshotHook.sol"; +import {D18} from "../../misc/types/D18.sol"; +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {AccountId} from "../../common/types/AccountId.sol"; +import {IValuation} from "../../common/interfaces/IValuation.sol"; + +interface INAVHook { + /// @notice Callback when there is a new net asset value (NAV) on a specific network. + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, D18 netAssetValue) external; +} + +interface INAVManager is ISnapshotHook { + //---------------------------------------------------------------------------------------------- + // Administration + //---------------------------------------------------------------------------------------------- + + /// @notice Get the NAV hook + function navHook() external view returns (INAVHook); + + /// @notice Set the NAV hook contract that will receive NAV updates + /// @param navHook The address of the NAV hook contract + function setNAVHook(INAVHook navHook) external; + + //---------------------------------------------------------------------------------------------- + // Account creation + //---------------------------------------------------------------------------------------------- + + /// @notice Initialize a new network by creating core accounts (equity, liability, gain, loss) + /// @param centrifugeId The Centrifuge ID of the network to initialize + function initializeNetwork(uint16 centrifugeId) external; + + /// @notice Initialize a new holding asset account and associate it with the hub + /// @param scId The share class ID + /// @param assetId The asset ID to initialize + /// @param valuation The valuation contract for this asset + function initializeHolding(ShareClassId scId, AssetId assetId, IValuation valuation) external; + + /// @notice Initialize a new liability account and associate it with the hub + /// @param scId The share class ID + /// @param assetId The asset ID to initialize as a liability + /// @param valuation The valuation contract for this liability + function initializeLiability(ShareClassId scId, AssetId assetId, IValuation valuation) external; + + //---------------------------------------------------------------------------------------------- + // Price updates + //---------------------------------------------------------------------------------------------- + + /// @notice Handle transfer events between networks + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param fromCentrifugeId The source network Centrifuge ID + /// @param toCentrifugeId The destination network Centrifuge ID + function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external; + + /// @notice Update the holding value for a specific asset + /// @param scId The share class ID + /// @param assetId The asset ID to update + function updateHoldingValue(ShareClassId scId, AssetId assetId) external; + + //---------------------------------------------------------------------------------------------- + // Calculations + //---------------------------------------------------------------------------------------------- + + /// @notice Calculate the net asset value for a specific network + /// @dev NAV = equity + gain - loss - liability + /// @param centrifugeId The Centrifuge ID of the network + /// @return The calculated net asset value + function netAssetValue(uint16 centrifugeId) external view returns (D18); + + //---------------------------------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------------------------------- + + /// @notice Get the asset account ID for a specific asset on a network + /// @param centrifugeId The Centrifuge ID of the network + /// @param assetId The asset ID + /// @return The account ID for the asset + function assetAccount(uint16 centrifugeId, AssetId assetId) external view returns (AccountId); + + /// @notice Get the expense account ID for a specific asset on a network + /// @param centrifugeId The Centrifuge ID of the network + /// @param assetId The asset ID + /// @return The account ID for the expense + function expenseAccount(uint16 centrifugeId, AssetId assetId) external view returns (AccountId); + + /// @notice Get the equity account ID for a specific network + /// @param centrifugeId The Centrifuge ID of the network + /// @return The equity account ID + function equityAccount(uint16 centrifugeId) external pure returns (AccountId); + + /// @notice Get the liability account ID for a specific network + /// @param centrifugeId The Centrifuge ID of the network + /// @return The liability account ID + function liabilityAccount(uint16 centrifugeId) external pure returns (AccountId); + + /// @notice Get the gain account ID for a specific network + /// @param centrifugeId The Centrifuge ID of the network + /// @return The gain account ID + function gainAccount(uint16 centrifugeId) external pure returns (AccountId); + + /// @notice Get the loss account ID for a specific network + /// @param centrifugeId The Centrifuge ID of the network + /// @return The loss account ID + function lossAccount(uint16 centrifugeId) external pure returns (AccountId); +} diff --git a/src/managers/interfaces/INAVManagerFactory.sol b/src/managers/interfaces/INAVManagerFactory.sol new file mode 100644 index 000000000..fbf0b000b --- /dev/null +++ b/src/managers/interfaces/INAVManagerFactory.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.28; + +import {INAVManager} from "./INAVManager.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; + +interface INAVManagerFactory { + event DeployNavManager(PoolId indexed poolId, address indexed manager); + + error InvalidPoolId(); + + /// @notice Deploys new merkle proof manager. + function newManager(PoolId poolId) external returns (INAVManager); +} diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol new file mode 100644 index 000000000..8db74c2a7 --- /dev/null +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {D18} from "../../misc/types/D18.sol"; +import {PoolId} from "../../common/types/PoolId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {INAVHook} from "./INavManager.sol"; + +interface ISimplePriceManager is INAVHook { + error InvalidShareClassCount(); + + struct NetworkMetrics { + D18 netAssetValue; + uint128 issuance; + } + + // function poolId() external view returns (PoolId); + // function scId() external view returns (ShareClassId); + // function networks(uint256 index) external view returns (uint16); + // function globalIssuance() external view returns (uint128); + // function globalNetAssetValue() external view returns (D18); + + function setNetworks(uint16[] calldata centrifugeIds) external; +} diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index 583448e2f..29a9d284e 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -45,6 +45,7 @@ import {IAsyncVault} from "../../src/vaults/interfaces/IAsyncVault.sol"; import {AsyncRequestManager} from "../../src/vaults/AsyncRequestManager.sol"; import {IAsyncRedeemVault} from "../../src/vaults/interfaces/IAsyncVault.sol"; +import {INAVHook} from "../../src/managers/interfaces/INAVManager.sol"; import {NAVManager} from "../../src/managers/NAVManager.sol"; import {SimplePriceManager} from "../../src/managers/SimplePriceManager.sol"; @@ -360,7 +361,7 @@ contract EndToEndFlows is EndToEndUtils { h.hub.addShareClass(POOL_A, "Tokenized MMF", "MMF", bytes32("salt")); SimplePriceManager priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub, address(this)); - h.navManager.setNAVHook(priceManager); + h.navManager.setNAVHook(INAVHook(address(priceManager))); h.hub.updateHubManager(POOL_A, address(h.navManager), true); h.hub.updateHubManager(POOL_A, address(priceManager), true); From 6e335ccbd041adc9c4c3f525fca16ee8b5beee10 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:53:52 +0200 Subject: [PATCH 36/83] onTransfer + approve --- src/common/interfaces/ISnapshotHook.sol | 8 +- src/hub/Hub.sol | 9 +- src/hub/ShareClassManager.sol | 3 + src/managers/NAVManager.sol | 39 ++++++-- src/managers/SimplePriceManager.sol | 82 ++++++++++++++++- src/managers/interfaces/INAVManager.sol | 32 +++++-- .../interfaces/ISimplePriceManager.sol | 22 +++++ test/hooks/mocks/MockSnapshotHook.sol | 13 ++- test/integration/EndToEnd.t.sol | 91 +++++++++++++------ test/integration/fork/ForkTestBase.sol | 4 +- test/integration/fork/ForkTestInvestments.sol | 4 +- 11 files changed, 253 insertions(+), 54 deletions(-) diff --git a/src/common/interfaces/ISnapshotHook.sol b/src/common/interfaces/ISnapshotHook.sol index e9005ce1b..f25440412 100644 --- a/src/common/interfaces/ISnapshotHook.sol +++ b/src/common/interfaces/ISnapshotHook.sol @@ -19,5 +19,11 @@ interface ISnapshotHook { function onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external; /// @notice Callback when there is a cross-chain transfer leading to issuance updates - function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external; + function onTransfer( + PoolId poolId, + ShareClassId scId, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, + uint128 sharesTransferred + ) external; } diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 0326d5637..fa2557636 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {console2} from "forge-std/console2.sol"; import {IHoldings} from "./interfaces/IHoldings.sol"; import {IHubHelpers} from "./interfaces/IHubHelpers.sol"; import {IHubRegistry} from "./interfaces/IHubRegistry.sol"; @@ -222,6 +223,8 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar (, D18 poolPerShare) = shareClassManager.metrics(scId); + console2.log("Hub notifySharePrice", centrifugeId); + emit NotifySharePrice(centrifugeId, poolId, scId, poolPerShare); sender.sendNotifyPricePoolPerShare(centrifugeId, poolId, scId, poolPerShare); } @@ -515,7 +518,7 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar /// @inheritdoc IHub function updateSharePrice(PoolId poolId, ShareClassId scId, D18 navPoolPerShare) public payable { _isManager(poolId); - + console2.log("SCM updateSharePrice from Hub", navPoolPerShare.raw()); shareClassManager.updateSharePrice(poolId, scId, navPoolPerShare); } @@ -685,6 +688,8 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar hubHelpers.updateAccountingAmount(poolId, scId, assetId, isIncrease, value); } + console2.log("Updated holding", amount); + holdings.setSnapshot(poolId, scId, centrifugeId, isSnapshot, nonce); } @@ -721,7 +726,7 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar shareClassManager.updateShares(targetCentrifugeId, poolId, scId, amount, true); ISnapshotHook hook = holdings.snapshotHook(poolId); - if (address(hook) != address(0)) hook.onTransfer(poolId, scId, originCentrifugeId, targetCentrifugeId); + if (address(hook) != address(0)) hook.onTransfer(poolId, scId, originCentrifugeId, targetCentrifugeId, amount); emit ForwardTransferShares(originCentrifugeId, targetCentrifugeId, poolId, scId, receiver, amount); sender.sendExecuteTransferShares( diff --git a/src/hub/ShareClassManager.sol b/src/hub/ShareClassManager.sol index 535de2345..6d178fed3 100644 --- a/src/hub/ShareClassManager.sol +++ b/src/hub/ShareClassManager.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {console2} from "forge-std/console2.sol"; import {IHubRegistry} from "./interfaces/IHubRegistry.sol"; import { IShareClassManager, @@ -336,6 +337,8 @@ contract ShareClassManager is Auth, IShareClassManager { function updateSharePrice(PoolId poolId, ShareClassId scId_, D18 navPoolPerShare) external auth { require(exists(poolId, scId_), ShareClassNotFound()); + console2.log("SCM updateSharePrice", navPoolPerShare.raw()); + ShareClassMetrics storage m = metrics[scId_]; m.navPerShare = navPoolPerShare; emit UpdateShareClass(poolId, scId_, navPoolPerShare); diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index ea7528dc6..5e2dc280e 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {console2} from "forge-std/console2.sol"; + import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; @@ -19,11 +21,6 @@ import {IAccounting} from "../hub/interfaces/IAccounting.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is Auth, INAVManager { - error AlreadyInitialized(); - error NotInitialized(); - error ExceedsMaxAccounts(); - error InvalidNAVHook(); - PoolId public immutable poolId; IHub public immutable hub; @@ -121,23 +118,44 @@ contract NAVManager is Auth, INAVManager { /// @inheritdoc ISnapshotHook function onSync(PoolId poolId_, ShareClassId scId, uint16 centrifugeId) external { + console2.log("NAVManager onSync"); require(poolId == poolId_); require(msg.sender == holdings, NotAuthorized()); require(address(navHook) != address(0), InvalidNAVHook()); D18 netAssetValue_ = netAssetValue(centrifugeId); + console2.log("NAV", netAssetValue_.raw()); navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); + console2.log("NAVManager onSync done"); } - function onTransfer(PoolId poolId_, ShareClassId scId_, uint16 fromCentrifugeId, uint16 toCentrifugeId) external { - // TODO + /// @inheritdoc ISnapshotHook + function onTransfer( + PoolId poolId_, + ShareClassId scId_, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, + uint128 sharesTransferred + ) external { + require(poolId == poolId_); + require(msg.sender == address(hub), NotAuthorized()); + require(address(navHook) != address(0), InvalidNAVHook()); + + navHook.onTransfer(poolId, scId_, fromCentrifugeId, toCentrifugeId, sharesTransferred); } function updateHoldingValue(ShareClassId scId, AssetId assetId) external { hub.updateHoldingValue(poolId, scId, assetId); } - // TODO: setHoldingAccountId, updateHoldingValuation + function updateHoldingValuation(ShareClassId scId, AssetId assetId, IValuation valuation) external { + hub.updateHoldingValuation(poolId, scId, assetId, valuation); + } + + function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) external { + hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); + } + // TODO: realize gain/loss to move to equity account //---------------------------------------------------------------------------------------------- @@ -151,6 +169,11 @@ contract NAVManager is Auth, INAVManager { (, uint128 gain) = accounting.accountValue(poolId, gainAccount(centrifugeId)); (, uint128 loss) = accounting.accountValue(poolId, lossAccount(centrifugeId)); (, uint128 liability) = accounting.accountValue(poolId, liabilityAccount(centrifugeId)); + + console2.log("Equity", equity); + console2.log("Gain", gain); + console2.log("Loss", loss); + console2.log("Liability", liability); return d18(equity) + d18(gain) - d18(loss) - d18(liability); } diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index cf339bc7a..57b351508 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {console2} from "forge-std/console2.sol"; import {INAVHook} from "./interfaces/INavManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; @@ -9,6 +10,7 @@ import {D18, d18} from "../misc/types/D18.sol"; import {IMulticall} from "../misc/interfaces/IMulticall.sol"; import {PoolId} from "../common/types/PoolId.sol"; +import {AssetId} from "../common/types/AssetId.sol"; import {ShareClassId} from "../common/types/ShareClassId.sol"; import {MAX_MESSAGE_COST} from "../common/interfaces/IGasService.sol"; @@ -52,30 +54,106 @@ contract SimplePriceManager is Auth, ISimplePriceManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVHook - function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external { + function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external auth { require(poolId == poolId_); require(scId == scId_); // TODO: check msg.sender + console2.log("SimplePriceManager onUpdate", netAssetValue.raw()); NetworkMetrics storage networkMetrics = metrics[centrifugeId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); + console2.log("SCM centid Issuance", issuance); + console2.log("Stored centid Issuance", networkMetrics.issuance); + globalIssuance = globalIssuance + issuance - networkMetrics.issuance; globalNetAssetValue = globalNetAssetValue + netAssetValue - networkMetrics.netAssetValue; D18 price = globalIssuance == 0 ? d18(1, 1) : globalNetAssetValue / d18(globalIssuance); + console2.log("price", price.raw()); + networkMetrics.netAssetValue = netAssetValue; networkMetrics.issuance = issuance; uint256 networkCount = networks.length; + console2.log("networkCount", networkCount); bytes[] memory cs = new bytes[](networkCount + 1); cs[0] = abi.encodeWithSelector(hub.updateSharePrice.selector, poolId, scId, price); for (uint256 i; i < networkCount; i++) { - cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, centrifugeId); + console2.log("Calling notifySharePrice for centid", networks[i]); + cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, networks[i]); } + console2.log("Calling multicall"); + IMulticall(address(hub)).multicall{value: MAX_MESSAGE_COST * (cs.length)}(cs); + + console2.log("SimplePriceManager onUpdate done"); + } + + /// @inheritdoc INAVHook + function onTransfer( + PoolId poolId_, + ShareClassId scId_, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, + uint128 sharesTransferred + ) external auth { + require(poolId == poolId_); + require(scId == scId_); + // TODO check msg.sender + + NetworkMetrics storage fromMetrics = metrics[fromCentrifugeId]; + NetworkMetrics storage toMetrics = metrics[toCentrifugeId]; + fromMetrics.issuance -= sharesTransferred; + toMetrics.issuance += sharesTransferred; + } + + //---------------------------------------------------------------------------------------------- + // Investor actions + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ISimplePriceManager + function approveDepositsAndIssueShares(AssetId depositAssetId, uint128 approvedAssetAmount, uint128 extraGasLimit) + external + auth + { + uint32 nowDepositEpochId = shareClassManager.nowDepositEpoch(scId, depositAssetId); + uint32 nowIssueEpochId = shareClassManager.nowIssueEpoch(scId, depositAssetId); + + require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); + + D18 navPoolPerShare = _navPerShare(); + hub.approveDeposits(poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount); + hub.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + } + + /// @inheritdoc ISimplePriceManager + function approveRedeemsAndRevokeShares(AssetId payoutAssetId, uint128 approvedShareAmount, uint128 extraGasLimit) + external + auth + { + uint32 nowRedeemEpochId = shareClassManager.nowRedeemEpoch(scId, payoutAssetId); + uint32 nowRevokeEpochId = shareClassManager.nowRevokeEpoch(scId, payoutAssetId); + + require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); + + D18 navPoolPerShare = _navPerShare(); + hub.approveRedeems(poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount); + hub.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + } + + //---------------------------------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------------------------------- + + function _navPerShare() internal view returns (D18) { + return globalIssuance == 0 ? d18(1, 1) : globalNetAssetValue / d18(globalIssuance); + } + + receive() external payable { + // Accept ETH refunds from multicall } } diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index 49fa1ba45..ec6f12560 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -11,10 +11,34 @@ import {IValuation} from "../../common/interfaces/IValuation.sol"; interface INAVHook { /// @notice Callback when there is a new net asset value (NAV) on a specific network. + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param centrifugeId The Centrifuge ID of the network + /// @param netAssetValue The new net asset value function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, D18 netAssetValue) external; + + /// @notice Handle transfer shares between networks + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param fromCentrifugeId The source network Centrifuge ID + /// @param toCentrifugeId The destination network Centrifuge ID + /// @param sharesTransferred The amount of shares transferred + function onTransfer( + PoolId poolId, + ShareClassId scId, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, + uint128 sharesTransferred + ) external; } interface INAVManager is ISnapshotHook { + error MismatchedEpochs(); + error AlreadyInitialized(); + error NotInitialized(); + error ExceedsMaxAccounts(); + error InvalidNAVHook(); + //---------------------------------------------------------------------------------------------- // Administration //---------------------------------------------------------------------------------------------- @@ -50,18 +74,12 @@ interface INAVManager is ISnapshotHook { // Price updates //---------------------------------------------------------------------------------------------- - /// @notice Handle transfer events between networks - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param fromCentrifugeId The source network Centrifuge ID - /// @param toCentrifugeId The destination network Centrifuge ID - function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external; - /// @notice Update the holding value for a specific asset /// @param scId The share class ID /// @param assetId The asset ID to update function updateHoldingValue(ShareClassId scId, AssetId assetId) external; + //---------------------------------------------------------------------------------------------- // Calculations //---------------------------------------------------------------------------------------------- diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol index 8db74c2a7..1a7e7a58e 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -3,11 +3,13 @@ pragma solidity 0.8.28; import {D18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {INAVHook} from "./INavManager.sol"; interface ISimplePriceManager is INAVHook { error InvalidShareClassCount(); + error MismatchedEpochs(); struct NetworkMetrics { D18 netAssetValue; @@ -21,4 +23,24 @@ interface ISimplePriceManager is INAVHook { // function globalNetAssetValue() external view returns (D18); function setNetworks(uint16[] calldata centrifugeIds) external; + + /// @notice Approve deposits and issue shares in sequence using current NAV per share + /// @param depositAssetId The asset ID for deposits + /// @param approvedAssetAmount Amount of assets to approve + /// @param extraGasLimit Extra gas limit for cross-chain operations + function approveDepositsAndIssueShares( + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external; + + /// @notice Approve redeems and revoke shares in sequence using current NAV per share + /// @param payoutAssetId The asset ID for payouts + /// @param approvedShareAmount Amount of shares to approve for redemption + /// @param extraGasLimit Extra gas limit for cross-chain operations + function approveRedeemsAndRevokeShares( + AssetId payoutAssetId, + uint128 approvedShareAmount, + uint128 extraGasLimit + ) external; } diff --git a/test/hooks/mocks/MockSnapshotHook.sol b/test/hooks/mocks/MockSnapshotHook.sol index c4ec10ca0..6c402c198 100644 --- a/test/hooks/mocks/MockSnapshotHook.sol +++ b/test/hooks/mocks/MockSnapshotHook.sol @@ -7,12 +7,21 @@ import {ISnapshotHook} from "../../../src/common/interfaces/ISnapshotHook.sol"; contract MockSnapshotHook is ISnapshotHook { mapping(PoolId => mapping(ShareClassId => mapping(uint16 centrifugeId => uint256 counter))) public synced; + mapping(PoolId => mapping(ShareClassId => mapping(uint16 centrifugeId => uint256 amountTransferred))) public + transfers; function onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external { synced[poolId][scId][centrifugeId]++; } - function onTransfer(PoolId poolId, ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId) external { - // TODO + function onTransfer( + PoolId poolId, + ShareClassId scId, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, + uint128 sharesTransferred + ) external { + transfers[poolId][scId][fromCentrifugeId] += sharesTransferred; + transfers[poolId][scId][toCentrifugeId] += sharesTransferred; } } diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index 60f68a057..574e7fd69 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -98,9 +98,8 @@ contract EndToEndDeployment is Test { Hub hub; // Others IdentityValuation identityValuation; - // MockValuation valuation; - // MockSnapshotHook snapshotHook; NAVManager navManager; + SimplePriceManager priceManager; OracleValuation oracleValuation; } @@ -215,6 +214,7 @@ contract EndToEndDeployment is Test { // valuation: new MockValuation(deployA.hubRegistry()), // snapshotHook: new MockSnapshotHook() navManager: new NAVManager(POOL_A, deployA.hub(), FM), + priceManager: SimplePriceManager(payable(0)), oracleValuation: deployA.oracleValuation() }); @@ -391,15 +391,6 @@ contract EndToEndFlows is EndToEndUtils { s_.spoke.registerAsset{value: GAS}(h.centrifugeId, address(s_.usdc), 0); } - // function _createPoolAccounts(CHub memory hub, PoolId poolId, address poolManager) internal { - // vm.startPrank(poolManager); - // hub.hub.createAccount(poolId, ASSET_ACCOUNT, true); - // hub.hub.createAccount(poolId, EQUITY_ACCOUNT, false); - // hub.hub.createAccount(poolId, LOSS_ACCOUNT, false); - // hub.hub.createAccount(poolId, GAIN_ACCOUNT, false); - // vm.stopPrank(); - // } - function _subsidizePool(CHub memory hub, PoolId poolId) internal { vm.startPrank(ANY); vm.deal(ANY, 1 ether); @@ -415,13 +406,7 @@ contract EndToEndFlows is EndToEndUtils { h.hub.setPoolMetadata(POOL_A, bytes("Testing pool")); h.hub.addShareClass(POOL_A, "Tokenized MMF", "MMF", bytes32("salt")); - SimplePriceManager priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub, address(this)); - h.navManager.setNAVHook(INAVHook(address(priceManager))); - - h.hub.updateHubManager(POOL_A, address(h.navManager), true); - h.hub.updateHubManager(POOL_A, address(priceManager), true); - // _createPoolAccounts(h, POOL_A, FM); - // _subsidizePool(h, POOL_A); + _subsidizePool(h, POOL_A); } function _configurePoolCrossChain( @@ -472,10 +457,30 @@ contract EndToEndFlows is EndToEndUtils { _configurePoolCrossChain(h, s_, POOL_A, SC_1, s_.usdcId, FM, address(s_.redemptionRestrictionsHook)); + vm.startPrank(FM); + h.priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub, address(this)); + vm.deal(address(h.priceManager), 1 ether); + h.navManager.setNAVHook(INAVHook(address(h.priceManager))); + console2.log("Hub network", h.centrifugeId); + console2.log("Spoke network", s_.centrifugeId); + // if (s_.centrifugeId != h.centrifugeId) { + // uint16[] memory networks = new uint16[](2); + // networks[0] = h.centrifugeId; + // networks[1] = s.centrifugeId; + // h.priceManager.setNetworks(networks); + // } else { + // uint16[] memory networks = new uint16[](1); + // console2.log("Hub network", h.centrifugeId); + // networks[0] = h.centrifugeId; + // h.priceManager.setNetworks(networks); + // } + + h.hub.updateHubManager(POOL_A, address(h.navManager), true); + h.hub.updateHubManager(POOL_A, address(h.priceManager), true); + h.navManager.initializeNetwork(s_.centrifugeId); h.navManager.initializeHolding(SC_1, s_.usdcId, h.oracleValuation); - vm.startPrank(FM); h.hub.setRequestManager{value: GAS}(POOL_A, SC_1, s_.usdcId, address(s.asyncRequestManager).toBytes32()); h.hub.updateBalanceSheetManager{value: GAS}( s_.centrifugeId, POOL_A, address(s.asyncRequestManager).toBytes32(), true @@ -483,7 +488,6 @@ contract EndToEndFlows is EndToEndUtils { h.hub.updateBalanceSheetManager{value: GAS}(s_.centrifugeId, POOL_A, address(s.syncManager).toBytes32(), true); h.hub.updateBalanceSheetManager{value: GAS}(s_.centrifugeId, POOL_A, BSM.toBytes32(), true); h.hub.setSnapshotHook(POOL_A, h.navManager); - // h.hub.setSnapshotHook(POOL_A, h.snapshotHook); h.oracleValuation.updateFeeder(POOL_A, FEEDER, true); h.hub.updateHubManager(POOL_A, address(h.oracleValuation), true); vm.stopPrank(); @@ -555,6 +559,11 @@ contract EndToEndFlows is EndToEndUtils { IAsyncVault vault = _ensureAsyncVaultExists(hub, spoke, poolId, shareClassId, assetId, poolManager, existingVault); + uint16[] memory networks = new uint16[](1); + console2.log("Spoke network", spoke.centrifugeId); + networks[0] = spoke.centrifugeId; + h.priceManager.setNetworks(networks); + // Execute deposit request _executeAsyncDepositRequest(vault, investor, amount); @@ -1027,12 +1036,22 @@ contract EndToEndFlows is EndToEndUtils { function _testUpdateAccountingAfterDeposit(bool sameChain, bool afterAsyncDeposit, bool nonZeroPrices) public { (afterAsyncDeposit) ? _testAsyncDeposit(sameChain, nonZeroPrices) : _testSyncDeposit(sameChain, nonZeroPrices); + (uint128 amount1,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); + console2.log("Holding amount after deposit, before sync assets:", amount1); vm.startPrank(BSM); + console2.log("Submitting queued assets"); s.balanceSheet.submitQueuedAssets(POOL_A, SC_1, s.usdcId, EXTRA_GAS); + (uint128 amount2,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); + console2.log("Holding amount after deposit, after sync assets:", amount2); + console2.log("Submitting queued shares"); s.balanceSheet.submitQueuedShares(POOL_A, SC_1, EXTRA_GAS); - + console2.log("Submitted"); + (uint128 amount3,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); + console2.log("Holding amount after deposit, after sync shares:", amount3); // CHECKS (uint128 amount, uint128 value,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); + console2.log("Holding amount after deposit:", amount); + console2.log("Holding value after deposit:", value); assertEq(amount, USDC_AMOUNT_1, "expected amount"); assertEq(value, assetToPool(USDC_AMOUNT_1), "expected value"); @@ -1151,7 +1170,6 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { h.hub.updateBalanceSheetManager{value: GAS}(s.centrifugeId, POOL_A, BSM.toBytes32(), true); h.hub.updateSharePrice(POOL_A, SC_1, IntegrationConstants.zeroPrice()); h.hub.notifySharePrice{value: GAS}(POOL_A, SC_1, s.centrifugeId); - // h.hub.setSnapshotHook(POOL_A, h.navManager); // Each message will return half of the gas wasted adapterBToA.setRefundedValue(h.gasService.updateShares() / 2); @@ -1194,8 +1212,9 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { } /// forge-config: default.isolate = true - function testFundManagement(bool sameChain) public { - _configurePool(sameChain); + function testFundManagement() public { + // bool sameChain + _configurePool(false); _configurePrices(IntegrationConstants.assetPrice(), IntegrationConstants.sharePrice()); vm.startPrank(ERC20_DEPLOYER); @@ -1204,14 +1223,26 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { vm.startPrank(BSM); s.usdc.approve(address(s.balanceSheet), USDC_AMOUNT_1); s.balanceSheet.deposit(POOL_A, SC_1, address(s.usdc), 0, USDC_AMOUNT_1); + + (uint128 amount1,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); + console2.log("Holding amount after deposit:", amount1); s.balanceSheet.withdraw(POOL_A, SC_1, address(s.usdc), 0, BSM, USDC_AMOUNT_1 * 4 / 5); + + (uint128 amount2,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); + console2.log("Holding amount after withdraw:", amount2); + vm.startPrank(BSM); + console2.log("Submitting queued assets"); + s.balanceSheet.submitQueuedAssets(POOL_A, SC_1, s.usdcId, EXTRA_GAS); + console2.log("Submitted"); // CHECKS assertEq(s.usdc.balanceOf(BSM), USDC_AMOUNT_1 * 4 / 5); assertEq(s.balanceSheet.availableBalanceOf(POOL_A, SC_1, address(s.usdc), 0), USDC_AMOUNT_1 / 5); (uint128 amount, uint128 value,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); + console2.log("Holding amount after sync:", amount); + console2.log("Holding value after sync:", value); assertEq(amount, USDC_AMOUNT_1 / 5); assertEq(value, assetToPool(USDC_AMOUNT_1 / 5)); @@ -1254,10 +1285,9 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { } /// forge-config: default.isolate = true - function testAsyncDepositCancel() public { - // bool sameChain, bool nonZeroPrices - _configurePool(false); - true + function testAsyncDepositCancel(bool sameChain, bool nonZeroPrices) public { + _configurePool(sameChain); + nonZeroPrices ? _configurePrices(IntegrationConstants.assetPrice(), IntegrationConstants.sharePrice()) : _configurePrices(IntegrationConstants.zeroPrice(), IntegrationConstants.zeroPrice()); @@ -1305,8 +1335,9 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { } /// forge-config: default.isolate = true - function testUpdateAccountingAfterDeposit_AfterAsyncDeposit(bool sameChain, bool nonZeroPrices) public { - _testUpdateAccountingAfterDeposit(sameChain, true, nonZeroPrices); + function testUpdateAccountingAfterDeposit_AfterAsyncDeposit() public { + // TODO bool sameChain, bool nonZeroPrices + _testUpdateAccountingAfterDeposit(false, true, true); } /// forge-config: default.isolate = true diff --git a/test/integration/fork/ForkTestBase.sol b/test/integration/fork/ForkTestBase.sol index 60a42aa7a..b0564fbe5 100644 --- a/test/integration/fork/ForkTestBase.sol +++ b/test/integration/fork/ForkTestBase.sol @@ -37,6 +37,7 @@ import {OracleValuation} from "../../../src/valuations/OracleValuation.sol"; import {IdentityValuation} from "../../../src/valuations/IdentityValuation.sol"; import {NAVManager} from "../../../src/managers/NAVManager.sol"; +import {SimplePriceManager} from "../../../src/managers/SimplePriceManager.sol"; import "forge-std/Test.sol"; @@ -78,7 +79,8 @@ contract ForkTestBase is EndToEndFlows { hub: Hub(IntegrationConstants.HUB), identityValuation: IdentityValuation(IntegrationConstants.IDENTITY_VALUATION), oracleValuation: OracleValuation(address(0)), // TODO: add this once deployed - navManager: NAVManager(address(0)) // Fork tests don't use snapshot hooks + navManager: NAVManager(address(0)), // Fork tests don't use snapshot hooks + priceManager: SimplePriceManager(payable(0)) // Fork tests doesn't use priceManager }); forkSpoke = CSpoke({ diff --git a/test/integration/fork/ForkTestInvestments.sol b/test/integration/fork/ForkTestInvestments.sol index f6cac1929..16410a514 100644 --- a/test/integration/fork/ForkTestInvestments.sol +++ b/test/integration/fork/ForkTestInvestments.sol @@ -34,6 +34,7 @@ import {IAsyncVault} from "../../../src/vaults/interfaces/IAsyncVault.sol"; import {AsyncRequestManager} from "../../../src/vaults/AsyncRequestManager.sol"; import {NAVManager} from "../../../src/managers/NAVManager.sol"; +import {SimplePriceManager} from "../../../src/managers/SimplePriceManager.sol"; import {FreezeOnly} from "../../../src/hooks/FreezeOnly.sol"; import {FullRestrictions} from "../../../src/hooks/FullRestrictions.sol"; @@ -390,7 +391,8 @@ contract ForkTestSyncInvestments is ForkTestBase, VMLabeling { hub: Hub(IntegrationConstants.HUB), identityValuation: IdentityValuation(IntegrationConstants.IDENTITY_VALUATION), oracleValuation: OracleValuation(address(0)), // TODO: add this once deployed - navManager: NAVManager(address(0)) // Fork tests don't use snapshot hooks + navManager: NAVManager(address(0)), // Fork tests don't use snapshot hooks + priceManager: SimplePriceManager(payable(0)) // Fork tests doesn't use priceManager }); forkSpoke = CSpoke({ From a9d4e6bd28f22202936b84173ed353aa80cdf427 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:55:18 +0200 Subject: [PATCH 37/83] manager tests --- src/managers/NAVManager.sol | 74 +++- src/managers/interfaces/INAVManager.sol | 41 +- test/managers/unit/NAVManager.t.sol | 511 ++++++++++++++++++++++++ 3 files changed, 605 insertions(+), 21 deletions(-) create mode 100644 test/managers/unit/NAVManager.t.sol diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 5e2dc280e..c46c8cf6c 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -30,6 +30,7 @@ contract NAVManager is Auth, INAVManager { INAVHook public navHook; mapping(uint16 centrifugeId => uint16) public accountCounter; mapping(uint16 centrifugeId => mapping(AssetId => AccountId)) public assetIdToAccountId; + mapping(address => bool) public manager; constructor(PoolId poolId_, IHub hub_, address deployer) Auth(deployer) { poolId = poolId_; @@ -39,22 +40,37 @@ contract NAVManager is Auth, INAVManager { accounting = hub.accounting(); } + /// @dev Check if the msg.sender is ward or a manager + modifier onlyManager() { + require(wards[msg.sender] == 1 || manager[msg.sender], NotAuthorized()); + _; + } + //---------------------------------------------------------------------------------------------- // Administration //---------------------------------------------------------------------------------------------- + /// @inheritdoc INAVManager function setNAVHook(INAVHook navHook_) external auth { navHook = navHook_; + emit SetNavHook(address(navHook_)); + } + + /// @inheritdoc INAVManager + function updateManager(address manager_, bool canManage) external auth { + require(manager_ != address(0), EmptyAddress()); + + manager[manager_] = canManage; + + emit UpdateManager(manager_, canManage); } //---------------------------------------------------------------------------------------------- // Account creation //---------------------------------------------------------------------------------------------- - function initializeNetwork(uint16 centrifugeId) external { - // TODO AUTH - // require(hubRegistry.manager(poolId, msg.sender), NotHubManager()); - + /// @inheritdoc INAVManager + function initializeNetwork(uint16 centrifugeId) external onlyManager { require(accountCounter[centrifugeId] == 0, AlreadyInitialized()); hub.createAccount(poolId, equityAccount(centrifugeId), false); @@ -63,10 +79,12 @@ contract NAVManager is Auth, INAVManager { hub.createAccount(poolId, lossAccount(centrifugeId), false); accountCounter[centrifugeId] = 5; + + emit InitializeNetwork(centrifugeId); } - function initializeHolding(ShareClassId scId, AssetId assetId, IValuation valuation) external { - // TODO AUTH + /// @inheritdoc INAVManager + function initializeHolding(ShareClassId scId, AssetId assetId, IValuation valuation) external onlyManager { uint16 centrifugeId = assetId.centrifugeId(); uint16 index = accountCounter[centrifugeId]; require(index > 0, NotInitialized()); @@ -91,10 +109,12 @@ contract NAVManager is Auth, INAVManager { ); accountCounter[centrifugeId] = index + 1; + + emit InitializeHolding(scId, assetId); } - function initializeLiability(ShareClassId scId, AssetId assetId, IValuation valuation) external { - // TODO AUTH + /// @inheritdoc INAVManager + function initializeLiability(ShareClassId scId, AssetId assetId, IValuation valuation) external onlyManager { uint16 centrifugeId = assetId.centrifugeId(); uint16 index = accountCounter[centrifugeId]; require(index > 0, NotInitialized()); @@ -110,6 +130,8 @@ contract NAVManager is Auth, INAVManager { hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount_, liabilityAccount(centrifugeId)); accountCounter[centrifugeId] = index + 1; + + emit InitializeLiability(scId, assetId); } //---------------------------------------------------------------------------------------------- @@ -119,14 +141,16 @@ contract NAVManager is Auth, INAVManager { /// @inheritdoc ISnapshotHook function onSync(PoolId poolId_, ShareClassId scId, uint16 centrifugeId) external { console2.log("NAVManager onSync"); - require(poolId == poolId_); require(msg.sender == holdings, NotAuthorized()); + require(poolId == poolId_, InvalidPoolId()); require(address(navHook) != address(0), InvalidNAVHook()); - D18 netAssetValue_ = netAssetValue(centrifugeId); - console2.log("NAV", netAssetValue_.raw()); + uint128 netAssetValue_ = netAssetValue(centrifugeId); + console2.log("NAV", netAssetValue_); navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); console2.log("NAVManager onSync done"); + + emit Sync(scId, centrifugeId, netAssetValue_); } /// @inheritdoc ISnapshotHook @@ -137,22 +161,30 @@ contract NAVManager is Auth, INAVManager { uint16 toCentrifugeId, uint128 sharesTransferred ) external { - require(poolId == poolId_); require(msg.sender == address(hub), NotAuthorized()); + require(poolId == poolId_, InvalidPoolId()); require(address(navHook) != address(0), InvalidNAVHook()); navHook.onTransfer(poolId, scId_, fromCentrifugeId, toCentrifugeId, sharesTransferred); + + emit Transfer(scId_, fromCentrifugeId, toCentrifugeId, sharesTransferred); } - function updateHoldingValue(ShareClassId scId, AssetId assetId) external { + /// @inheritdoc INAVManager + function updateHoldingValue(ShareClassId scId, AssetId assetId) external onlyManager { hub.updateHoldingValue(poolId, scId, assetId); } - function updateHoldingValuation(ShareClassId scId, AssetId assetId, IValuation valuation) external { + /// @inheritdoc INAVManager + function updateHoldingValuation(ShareClassId scId, AssetId assetId, IValuation valuation) external onlyManager { hub.updateHoldingValuation(poolId, scId, assetId, valuation); } - function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) external { + /// @inheritdoc INAVManager + function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) + external + onlyManager + { hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); } @@ -162,8 +194,8 @@ contract NAVManager is Auth, INAVManager { // Calculations //---------------------------------------------------------------------------------------------- - /// @dev NAV = equity + gain - loss - liability - function netAssetValue(uint16 centrifugeId) public view returns (D18) { + /// @inheritdoc INAVManager + function netAssetValue(uint16 centrifugeId) public view returns (uint128) { // TODO: how to handle when one of the accounts is not positive (, uint128 equity) = accounting.accountValue(poolId, equityAccount(centrifugeId)); (, uint128 gain) = accounting.accountValue(poolId, gainAccount(centrifugeId)); @@ -174,33 +206,39 @@ contract NAVManager is Auth, INAVManager { console2.log("Gain", gain); console2.log("Loss", loss); console2.log("Liability", liability); - return d18(equity) + d18(gain) - d18(loss) - d18(liability); + return equity + gain - loss - liability; } //---------------------------------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------------------------------- + /// @inheritdoc INAVManager function assetAccount(uint16 centrifugeId, AssetId assetId) public view returns (AccountId) { return assetIdToAccountId[centrifugeId][assetId]; } + /// @inheritdoc INAVManager function expenseAccount(uint16 centrifugeId, AssetId assetId) public view returns (AccountId) { return assetAccount(centrifugeId, assetId); } + /// @inheritdoc INAVManager function equityAccount(uint16 centrifugeId) public pure returns (AccountId) { return withCentrifugeId(centrifugeId, 1); } + /// @inheritdoc INAVManager function liabilityAccount(uint16 centrifugeId) public pure returns (AccountId) { return withCentrifugeId(centrifugeId, 2); } + /// @inheritdoc INAVManager function gainAccount(uint16 centrifugeId) public pure returns (AccountId) { return withCentrifugeId(centrifugeId, 3); } + /// @inheritdoc INAVManager function lossAccount(uint16 centrifugeId) public pure returns (AccountId) { return withCentrifugeId(centrifugeId, 4); } diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index ec6f12560..8a9ea175a 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -15,7 +15,7 @@ interface INAVHook { /// @param scId The share class ID /// @param centrifugeId The Centrifuge ID of the network /// @param netAssetValue The new net asset value - function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, D18 netAssetValue) external; + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external; /// @notice Handle transfer shares between networks /// @param poolId The pool ID @@ -33,11 +33,26 @@ interface INAVHook { } interface INAVManager is ISnapshotHook { + event UpdateManager(address indexed manager, bool canManage); + event SetNavHook(address indexed navHook); + event InitializeNetwork(uint16 indexed centrifugeId); + event InitializeHolding(ShareClassId indexed scId, AssetId indexed assetId); + event InitializeLiability(ShareClassId indexed scId, AssetId indexed assetId); + event Sync(ShareClassId indexed scId, uint16 indexed centrifugeId, uint128 netAssetValue); + event Transfer( + ShareClassId indexed scId_, + uint16 indexed fromCentrifugeId, + uint16 indexed toCentrifugeId, + uint128 sharesTransferred + ); + error MismatchedEpochs(); error AlreadyInitialized(); error NotInitialized(); error ExceedsMaxAccounts(); error InvalidNAVHook(); + error InvalidPoolId(); + error EmptyAddress(); //---------------------------------------------------------------------------------------------- // Administration @@ -50,6 +65,14 @@ interface INAVManager is ISnapshotHook { /// @param navHook The address of the NAV hook contract function setNAVHook(INAVHook navHook) external; + /// @notice Check if an address can manage the NAV manager + function manager(address manager) external view returns (bool); + + /// @notice Update whether an address can manage the NAV manager + /// @param manager The address of the manager + /// @param canManage Whether the address can manage this manager + function updateManager(address manager, bool canManage) external; + //---------------------------------------------------------------------------------------------- // Account creation //---------------------------------------------------------------------------------------------- @@ -71,7 +94,7 @@ interface INAVManager is ISnapshotHook { function initializeLiability(ShareClassId scId, AssetId assetId, IValuation valuation) external; //---------------------------------------------------------------------------------------------- - // Price updates + // Holding updates //---------------------------------------------------------------------------------------------- /// @notice Update the holding value for a specific asset @@ -79,6 +102,18 @@ interface INAVManager is ISnapshotHook { /// @param assetId The asset ID to update function updateHoldingValue(ShareClassId scId, AssetId assetId) external; + /// @notice Update the valuation contract for a specific asset + /// @param scId The share class ID + /// @param assetId The asset ID to update + /// @param valuation The new valuation contract + function updateHoldingValuation(ShareClassId scId, AssetId assetId, IValuation valuation) external; + + /// @notice Set the account ID for a specific asset holding + /// @param scId The share class ID + /// @param assetId The asset ID + /// @param kind The account kind (type) + /// @param accountId The account ID to set + function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) external; //---------------------------------------------------------------------------------------------- // Calculations @@ -88,7 +123,7 @@ interface INAVManager is ISnapshotHook { /// @dev NAV = equity + gain - loss - liability /// @param centrifugeId The Centrifuge ID of the network /// @return The calculated net asset value - function netAssetValue(uint16 centrifugeId) external view returns (D18); + function netAssetValue(uint16 centrifugeId) external view returns (uint128); //---------------------------------------------------------------------------------------------- // Helpers diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/unit/NAVManager.t.sol new file mode 100644 index 000000000..3c0cf3247 --- /dev/null +++ b/test/managers/unit/NAVManager.t.sol @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; +import {D18, d18} from "../../../src/misc/types/D18.sol"; + +import {PoolId} from "../../../src/common/types/PoolId.sol"; +import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; +import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; +import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; +import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; + +import {NAVManager, NavManagerFactory} from "../../../src/managers/NAVManager.sol"; +import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; +import {INAVManagerFactory} from "../../../src/managers/interfaces/INAVManagerFactory.sol"; + +import {IHub} from "../../../src/hub/interfaces/IHub.sol"; +import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; +import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; + +import {MockValuation} from "../../common/mocks/MockValuation.sol"; +import {Mock} from "../../common/mocks/Mock.sol"; + +import "forge-std/Test.sol"; + +// Mock contracts to bypass foundry mockCall issues +contract IsContract {} + +contract NAVManagerTest is Test { + PoolId constant POOL_A = PoolId.wrap(1); + PoolId constant POOL_B = PoolId.wrap(2); + ShareClassId constant SC_1 = ShareClassId.wrap(bytes16("1")); + ShareClassId constant SC_2 = ShareClassId.wrap(bytes16("2")); + uint16 constant CENTRIFUGE_ID_1 = 1; + uint16 constant CENTRIFUGE_ID_2 = 2; + + AssetId asset1 = newAssetId(1, 1); + AssetId asset2 = newAssetId(2, 1); + + address hub = address(new IsContract()); + address accounting = address(new IsContract()); + address holdings = address(new IsContract()); + address hubRegistry = address(new IsContract()); + INAVHook navHook = INAVHook(address(new IsContract())); + + address deployer = address(this); + address contractUpdater = makeAddr("contractUpdater"); + address unauthorized = makeAddr("unauthorized"); + address manager = makeAddr("manager"); + + NAVManager navManager; + MockValuation mockValuation; + + function setUp() public virtual { + _setupMocks(); + _deployManager(); + + mockValuation = new MockValuation(IHubRegistry(hubRegistry)); + mockValuation.setPrice(POOL_A, SC_1, asset1, d18(1, 1)); + } + + function _setupMocks() internal { + vm.mockCall(hub, abi.encodeWithSelector(IHub.accounting.selector), abi.encode(accounting)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.holdings.selector), abi.encode(holdings)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.createAccount.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.initializeHolding.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.initializeLiability.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.updateHoldingValue.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.updateHoldingValuation.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.setHoldingAccountId.selector), abi.encode()); + + vm.mockCall(accounting, abi.encodeWithSelector(IAccounting.accountValue.selector), abi.encode(true, uint128(0))); + + vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint128)", asset1), abi.encode(6)); + vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint128)", asset2), abi.encode(6)); + vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint64)", POOL_A), abi.encode(18)); + + vm.mockCall(address(navHook), abi.encodeWithSelector(INAVHook.onUpdate.selector), abi.encode()); + vm.mockCall(address(navHook), abi.encodeWithSelector(INAVHook.onTransfer.selector), abi.encode()); + } + + function _deployManager() internal { + vm.prank(deployer); + navManager = new NAVManager(POOL_A, IHub(hub), deployer); + } + + function _mockAccountValue(AccountId accountId, uint128 value, bool isPositive) internal { + vm.mockCall( + address(accounting), + abi.encodeWithSelector(IAccounting.accountValue.selector, POOL_A, accountId), + abi.encode(isPositive, value) + ); + } +} + +contract NAVManagerConstructorTest is NAVManagerTest { + function testConstructor() public view { + assertEq(PoolId.unwrap(navManager.poolId()), PoolId.unwrap(POOL_A)); + assertEq(address(navManager.hub()), address(hub)); + assertEq(navManager.holdings(), holdings); + assertEq(address(navManager.accounting()), address(accounting)); + assertEq(address(navManager.navHook()), address(0)); + } +} + +contract NAVManagerConfigureTest is NAVManagerTest { + function testSetNAVHookSuccess() public { + vm.prank(deployer); + navManager.setNAVHook(navHook); + + assertEq(address(navManager.navHook()), address(navHook)); + } + + function testSetNAVHookUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + navManager.setNAVHook(navHook); + } + + function testSetNAVHookToZeroAddress() public { + vm.prank(deployer); + navManager.setNAVHook(INAVHook(address(0))); + + assertEq(address(navManager.navHook()), address(0)); + } + + function testInitializeNetworkSuccess() public { + vm.expectCall( + address(hub), + abi.encodeWithSelector( + IHub.createAccount.selector, POOL_A, navManager.equityAccount(CENTRIFUGE_ID_1), false + ) + ); + vm.expectCall( + address(hub), + abi.encodeWithSelector( + IHub.createAccount.selector, POOL_A, navManager.liabilityAccount(CENTRIFUGE_ID_1), false + ) + ); + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, navManager.gainAccount(CENTRIFUGE_ID_1), false) + ); + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, navManager.lossAccount(CENTRIFUGE_ID_1), false) + ); + + navManager.initializeNetwork(CENTRIFUGE_ID_1); + + assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 5); + } + + function testInitializeNetworkAlreadyInitialized() public { + navManager.initializeNetwork(CENTRIFUGE_ID_1); + + vm.expectRevert(INAVManager.AlreadyInitialized.selector); + navManager.initializeNetwork(CENTRIFUGE_ID_1); + } +} + +contract NAVManagerHoldingInitializationTest is NAVManagerTest { + function setUp() public override { + super.setUp(); + // Initialize network first + navManager.initializeNetwork(CENTRIFUGE_ID_1); + } + + function testInitializeHoldingSuccess() public { + AccountId expectedAssetAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); + + vm.expectCall( + address(hub), abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, expectedAssetAccount, true) + ); + vm.expectCall( + address(hub), + abi.encodeWithSelector( + IHub.initializeHolding.selector, + POOL_A, + SC_1, + asset1, + mockValuation, + expectedAssetAccount, + navManager.equityAccount(CENTRIFUGE_ID_1), + navManager.gainAccount(CENTRIFUGE_ID_1), + navManager.lossAccount(CENTRIFUGE_ID_1) + ) + ); + + navManager.initializeHolding(SC_1, asset1, mockValuation); + + assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 6); + assertEq( + AccountId.unwrap(navManager.assetAccount(CENTRIFUGE_ID_1, asset1)), AccountId.unwrap(expectedAssetAccount) + ); + } + + function testInitializeHoldingNotInitialized() public { + vm.expectRevert(INAVManager.NotInitialized.selector); + navManager.initializeHolding(SC_1, AssetId.wrap(uint128(3) << 64 | 300), mockValuation); + } + + function testInitializeHoldingSameAssetTwice() public { + navManager.initializeHolding(SC_1, asset1, mockValuation); + + // Should reuse the same account + AccountId expectedAssetAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); + + vm.expectCall( + address(hub), abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, expectedAssetAccount, true) + ); + + navManager.initializeHolding(SC_2, asset1, mockValuation); + + // Account counter should increment again + assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 7); + } +} + +contract NAVManagerLiabilityInitializationTest is NAVManagerTest { + function setUp() public override { + super.setUp(); + navManager.initializeNetwork(CENTRIFUGE_ID_1); + } + + function testInitializeLiabilitySuccess() public { + AccountId expectedExpenseAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); + + vm.expectCall( + address(hub), abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, expectedExpenseAccount, true) + ); + vm.expectCall( + address(hub), + abi.encodeWithSelector( + IHub.initializeLiability.selector, + POOL_A, + SC_1, + asset1, + mockValuation, + expectedExpenseAccount, + navManager.liabilityAccount(CENTRIFUGE_ID_1) + ) + ); + + navManager.initializeLiability(SC_1, asset1, mockValuation); + + assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 6); + assertEq( + AccountId.unwrap(navManager.expenseAccount(CENTRIFUGE_ID_1, asset1)), + AccountId.unwrap(expectedExpenseAccount) + ); + } + + function testInitializeLiabilityNotInitialized() public { + vm.expectRevert(INAVManager.NotInitialized.selector); + navManager.initializeLiability(SC_1, asset2, mockValuation); + } +} + +contract NAVManagerOnSyncTest is NAVManagerTest { + function setUp() public override { + super.setUp(); + navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.setNAVHook(navHook); + } + + function testOnSyncSuccess() public { + // Mock account values: equity=1000, gain=200, loss=100, liability=50 + // NAV = 1000 + 200 - 100 - 50 = 1050 + _mockAccountValue(navManager.equityAccount(CENTRIFUGE_ID_1), 1000, true); + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 200, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 100, false); + _mockAccountValue(navManager.liabilityAccount(CENTRIFUGE_ID_1), 50, true); + + vm.expectCall( + address(navHook), + abi.encodeWithSelector(INAVHook.onUpdate.selector, POOL_A, SC_1, CENTRIFUGE_ID_1, d18(1050)) + ); + + vm.prank(holdings); + navManager.onSync(POOL_A, SC_1, CENTRIFUGE_ID_1); + + // assertEq(mockNAVHook.updateCallCount(), 1); + // assertEq(PoolId.unwrap(mockNAVHook.lastPoolId()), PoolId.unwrap(POOL_A)); + // assertEq(ShareClassId.unwrap(mockNAVHook.lastScId()), ShareClassId.unwrap(SC_1)); + // assertEq(mockNAVHook.lastCentrifugeId(), CENTRIFUGE_ID_1); + // assertEq(mockNAVHook.lastNetAssetValue().raw(), d18(1050).raw()); + } + + function testOnSyncInvalidPoolId() public { + vm.expectRevert(); + vm.prank(holdings); + navManager.onSync(POOL_B, SC_1, CENTRIFUGE_ID_1); + } + + function testOnSyncNotHoldings() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + navManager.onSync(POOL_A, SC_1, CENTRIFUGE_ID_1); + } + + function testOnSyncNoNAVHook() public { + // Reset NAV hook to zero + vm.prank(deployer); + navManager.setNAVHook(INAVHook(address(0))); + + vm.expectRevert(INAVManager.InvalidNAVHook.selector); + vm.prank(holdings); + navManager.onSync(POOL_A, SC_1, CENTRIFUGE_ID_1); + } +} + +contract NAVManagerNetAssetValueTest is NAVManagerTest { + function testNetAssetValueCalculation() public { + // Mock account values: equity=1000, gain=200, loss=100, liability=50 + // Expected NAV = 1000 + 200 - 100 - 50 = 1050 + _mockAccountValue(navManager.equityAccount(CENTRIFUGE_ID_1), 1000, true); + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 200, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 100, false); + _mockAccountValue(navManager.liabilityAccount(CENTRIFUGE_ID_1), 50, true); + + uint128 nav = navManager.netAssetValue(CENTRIFUGE_ID_1); + assertEq(nav, 1050); + } + + function testNetAssetValueZero() public view { + uint128 nav = navManager.netAssetValue(CENTRIFUGE_ID_1); + assertEq(nav, 0); + } + + function testNetAssetValueNegative() public { + // Mock values that result in negative NAV + // equity=100, gain=50, loss=200, liability=100 + // NAV = 100 + 50 - 200 - 100 = -150 + _mockAccountValue(navManager.equityAccount(CENTRIFUGE_ID_1), 100, true); + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 50, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 200, false); + _mockAccountValue(navManager.liabilityAccount(CENTRIFUGE_ID_1), 100, true); + + vm.expectRevert(); + uint128 nav = navManager.netAssetValue(CENTRIFUGE_ID_1); + } +} + +contract NAVManagerUpdateHoldingTest is NAVManagerTest { + function testUpdateHoldingValue() public { + vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateHoldingValue.selector, POOL_A, SC_1, asset1)); + + navManager.updateHoldingValue(SC_1, asset1); + } + + function testUpdateHoldingValuation() public { + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHub.updateHoldingValuation.selector, POOL_A, SC_1, asset1, mockValuation) + ); + + navManager.updateHoldingValuation(SC_1, asset1, mockValuation); + } + + function testSetHoldingAccountId() public { + AccountId accountId = withCentrifugeId(CENTRIFUGE_ID_1, 10); + uint8 kind = 1; + + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHub.setHoldingAccountId.selector, POOL_A, SC_1, asset1, kind, accountId) + ); + + navManager.setHoldingAccountId(SC_1, asset1, kind, accountId); + } +} + +contract NAVManagerHelperFunctionsTest is NAVManagerTest { + function testEquityAccount() public view { + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 1); + AccountId actual = navManager.equityAccount(CENTRIFUGE_ID_1); + assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + } + + function testLiabilityAccount() public view { + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 2); + AccountId actual = navManager.liabilityAccount(CENTRIFUGE_ID_1); + assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + } + + function testGainAccount() public view { + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 3); + AccountId actual = navManager.gainAccount(CENTRIFUGE_ID_1); + assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + } + + function testLossAccount() public view { + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 4); + AccountId actual = navManager.lossAccount(CENTRIFUGE_ID_1); + assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + } + + function testAssetAccount() public { + navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeHolding(SC_1, asset1, mockValuation); + + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); + AccountId actual = navManager.assetAccount(CENTRIFUGE_ID_1, asset1); + assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + } + + function testExpenseAccount() public { + navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeLiability(SC_1, asset1, mockValuation); + + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); + AccountId actual = navManager.expenseAccount(CENTRIFUGE_ID_1, asset1); + assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + } + + function testAssetAccountNotInitialized() public view { + AccountId actual = navManager.assetAccount(CENTRIFUGE_ID_1, asset1); + assertTrue(actual.isNull()); + } +} + +contract NAVManagerOnTransferTest is NAVManagerTest { + function setUp() public override { + super.setUp(); + navManager.setNAVHook(navHook); + } + + function testOnTransferBasicAuth() public { + vm.expectRevert(); + vm.prank(unauthorized); + navManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); + } + + function testOnTransferInvalidPoolId() public { + vm.expectRevert(); + vm.prank(hub); + navManager.onTransfer(POOL_B, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); + } + + function testOnTransferNoNAVHook() public { + vm.prank(deployer); + navManager.setNAVHook(INAVHook(address(0))); + + vm.expectRevert(INAVManager.InvalidNAVHook.selector); + vm.prank(hub); + navManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); + } + + function testOnTransferSuccess() public { + vm.expectCall( + address(navHook), + abi.encodeWithSelector(INAVHook.onTransfer.selector, POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, d18(1)) + ); + + vm.prank(hub); + navManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); + } +} + +contract NAVManagerFactoryTest is Test { + address hub = address(new IsContract()); + address accounting = address(new IsContract()); + address holdings = address(new IsContract()); + address hubRegistry = address(new IsContract()); + address contractUpdater = makeAddr("contractUpdater"); + + NavManagerFactory factory; + + function setUp() public { + vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.accounting.selector), abi.encode(accounting)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.holdings.selector), abi.encode(holdings)); + factory = new NavManagerFactory(contractUpdater, IHub(hub)); + } + + function testFactoryConstructor() public view { + assertEq(factory.contractUpdater(), contractUpdater); + assertEq(address(factory.hub()), address(hub)); + } + + function testNewManagerSuccess() public { + PoolId poolId = PoolId.wrap(1); + + // Mock hubRegistry.exists() to return true + vm.mockCall( + address(hubRegistry), abi.encodeWithSelector(IHubRegistry.exists.selector, poolId), abi.encode(true) + ); + + vm.expectEmit(true, false, false, false); + emit INAVManagerFactory.DeployNavManager(poolId, address(0)); // address will be different + + INAVManager manager = factory.newManager(poolId); + + assertTrue(address(manager) != address(0)); + assertEq(PoolId.unwrap(NAVManager(address(manager)).poolId()), PoolId.unwrap(poolId)); + } + + function testNewManagerInvalidPool() public { + PoolId poolId = PoolId.wrap(1); + + // Mock hubRegistry.exists() to return false + vm.mockCall( + address(hubRegistry), abi.encodeWithSelector(IHubRegistry.exists.selector, poolId), abi.encode(false) + ); + + vm.expectRevert(INAVManagerFactory.InvalidPoolId.selector); + factory.newManager(poolId); + } +} From 93b94fbc6957eafdfd5e555f19525e36b3c98238 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:22:54 +0200 Subject: [PATCH 38/83] revert e2e changes --- test/integration/EndToEnd.t.sol | 170 ++++++------------ test/integration/fork/ForkTestBase.sol | 6 +- test/integration/fork/ForkTestInvestments.sol | 6 +- 3 files changed, 55 insertions(+), 127 deletions(-) diff --git a/test/integration/EndToEnd.t.sol b/test/integration/EndToEnd.t.sol index 574e7fd69..f9fa1792c 100644 --- a/test/integration/EndToEnd.t.sol +++ b/test/integration/EndToEnd.t.sol @@ -6,7 +6,7 @@ import {LocalAdapter} from "./adapters/LocalAdapter.sol"; import {IntegrationConstants} from "./utils/IntegrationConstants.sol"; import {ERC20} from "../../src/misc/ERC20.sol"; -import {D18, d18} from "../../src/misc/types/D18.sol"; +import {D18} from "../../src/misc/types/D18.sol"; import {IAuth} from "../../src/misc/interfaces/IAuth.sol"; import {IERC20} from "../../src/misc/interfaces/IERC20.sol"; import {CastLib} from "../../src/misc/libraries/CastLib.sol"; @@ -46,9 +46,7 @@ import {IAsyncVault} from "../../src/vaults/interfaces/IAsyncVault.sol"; import {AsyncRequestManager} from "../../src/vaults/AsyncRequestManager.sol"; import {IAsyncRedeemVault} from "../../src/vaults/interfaces/IAsyncVault.sol"; -import {INAVHook} from "../../src/managers/interfaces/INAVManager.sol"; -import {NAVManager} from "../../src/managers/NAVManager.sol"; -import {SimplePriceManager} from "../../src/managers/SimplePriceManager.sol"; +import {MockSnapshotHook} from "../hooks/mocks/MockSnapshotHook.sol"; import {FreezeOnly} from "../../src/hooks/FreezeOnly.sol"; import {FullRestrictions} from "../../src/hooks/FullRestrictions.sol"; @@ -98,9 +96,8 @@ contract EndToEndDeployment is Test { Hub hub; // Others IdentityValuation identityValuation; - NAVManager navManager; - SimplePriceManager priceManager; OracleValuation oracleValuation; + MockSnapshotHook snapshotHook; } struct CSpoke { @@ -144,10 +141,10 @@ contract EndToEndDeployment is Test { uint128 constant USDC_AMOUNT_1 = IntegrationConstants.DEFAULT_USDC_AMOUNT; - // AccountId constant ASSET_ACCOUNT = IntegrationConstants.ASSET_ACCOUNT; - // AccountId constant EQUITY_ACCOUNT = IntegrationConstants.EQUITY_ACCOUNT; - // AccountId constant LOSS_ACCOUNT = IntegrationConstants.LOSS_ACCOUNT; - // AccountId constant GAIN_ACCOUNT = IntegrationConstants.GAIN_ACCOUNT; + AccountId constant ASSET_ACCOUNT = IntegrationConstants.ASSET_ACCOUNT; + AccountId constant EQUITY_ACCOUNT = IntegrationConstants.EQUITY_ACCOUNT; + AccountId constant LOSS_ACCOUNT = IntegrationConstants.LOSS_ACCOUNT; + AccountId constant GAIN_ACCOUNT = IntegrationConstants.GAIN_ACCOUNT; AssetId USD_ID; PoolId POOL_A; @@ -191,14 +188,6 @@ contract EndToEndDeployment is Test { vm.deal(INVESTOR_A, 1 ether); vm.deal(ANY, 1 ether); - // We not use the VM chain - vm.chainId(0xDEAD); - - // Initialize default values - USD_ID = deployA.USD_ID(); - POOL_A = deployA.hubRegistry().poolId(CENTRIFUGE_ID_A, 1); - SC_1 = deployA.shareClassManager().previewNextShareClassId(POOL_A); - h = CHub({ centrifugeId: CENTRIFUGE_ID_A, root: deployA.root(), @@ -211,13 +200,15 @@ contract EndToEndDeployment is Test { shareClassManager: deployA.shareClassManager(), hub: deployA.hub(), identityValuation: deployA.identityValuation(), - // valuation: new MockValuation(deployA.hubRegistry()), - // snapshotHook: new MockSnapshotHook() - navManager: new NAVManager(POOL_A, deployA.hub(), FM), - priceManager: SimplePriceManager(payable(0)), - oracleValuation: deployA.oracleValuation() + oracleValuation: deployA.oracleValuation(), + snapshotHook: new MockSnapshotHook() }); + // Initialize default values + USD_ID = deployA.USD_ID(); + POOL_A = h.hubRegistry.poolId(CENTRIFUGE_ID_A, 1); + SC_1 = h.shareClassManager.previewNextShareClassId(POOL_A); + vm.label(address(adapterAToB), "AdapterAToB"); vm.label(address(adapterBToA), "AdapterBToA"); } @@ -391,6 +382,15 @@ contract EndToEndFlows is EndToEndUtils { s_.spoke.registerAsset{value: GAS}(h.centrifugeId, address(s_.usdc), 0); } + function _createPoolAccounts(CHub memory hub, PoolId poolId, address poolManager) internal { + vm.startPrank(poolManager); + hub.hub.createAccount(poolId, ASSET_ACCOUNT, true); + hub.hub.createAccount(poolId, EQUITY_ACCOUNT, false); + hub.hub.createAccount(poolId, LOSS_ACCOUNT, false); + hub.hub.createAccount(poolId, GAIN_ACCOUNT, false); + vm.stopPrank(); + } + function _subsidizePool(CHub memory hub, PoolId poolId) internal { vm.startPrank(ANY); vm.deal(ANY, 1 ether); @@ -406,6 +406,7 @@ contract EndToEndFlows is EndToEndUtils { h.hub.setPoolMetadata(POOL_A, bytes("Testing pool")); h.hub.addShareClass(POOL_A, "Tokenized MMF", "MMF", bytes32("salt")); + _createPoolAccounts(h, POOL_A, FM); _subsidizePool(h, POOL_A); } @@ -424,16 +425,16 @@ contract EndToEndFlows is EndToEndUtils { hub.hub.notifyPool{value: GAS}(poolId, spoke.centrifugeId); hub.hub.notifyShareClass{value: GAS}(poolId, shareClassId, spoke.centrifugeId, hookAddress.toBytes32()); - // hub.hub.initializeHolding( - // poolId, - // shareClassId, - // assetId, - // hub.oracleValuation, - // ASSET_ACCOUNT, - // EQUITY_ACCOUNT, - // GAIN_ACCOUNT, - // LOSS_ACCOUNT - // ); + hub.hub.initializeHolding( + poolId, + shareClassId, + assetId, + hub.oracleValuation, + ASSET_ACCOUNT, + EQUITY_ACCOUNT, + GAIN_ACCOUNT, + LOSS_ACCOUNT + ); hub.hub.setRequestManager{value: GAS}( poolId, shareClassId, assetId, address(spoke.asyncRequestManager).toBytes32() ); @@ -458,36 +459,7 @@ contract EndToEndFlows is EndToEndUtils { _configurePoolCrossChain(h, s_, POOL_A, SC_1, s_.usdcId, FM, address(s_.redemptionRestrictionsHook)); vm.startPrank(FM); - h.priceManager = new SimplePriceManager(POOL_A, SC_1, h.hub, address(this)); - vm.deal(address(h.priceManager), 1 ether); - h.navManager.setNAVHook(INAVHook(address(h.priceManager))); - console2.log("Hub network", h.centrifugeId); - console2.log("Spoke network", s_.centrifugeId); - // if (s_.centrifugeId != h.centrifugeId) { - // uint16[] memory networks = new uint16[](2); - // networks[0] = h.centrifugeId; - // networks[1] = s.centrifugeId; - // h.priceManager.setNetworks(networks); - // } else { - // uint16[] memory networks = new uint16[](1); - // console2.log("Hub network", h.centrifugeId); - // networks[0] = h.centrifugeId; - // h.priceManager.setNetworks(networks); - // } - - h.hub.updateHubManager(POOL_A, address(h.navManager), true); - h.hub.updateHubManager(POOL_A, address(h.priceManager), true); - - h.navManager.initializeNetwork(s_.centrifugeId); - h.navManager.initializeHolding(SC_1, s_.usdcId, h.oracleValuation); - - h.hub.setRequestManager{value: GAS}(POOL_A, SC_1, s_.usdcId, address(s.asyncRequestManager).toBytes32()); - h.hub.updateBalanceSheetManager{value: GAS}( - s_.centrifugeId, POOL_A, address(s.asyncRequestManager).toBytes32(), true - ); - h.hub.updateBalanceSheetManager{value: GAS}(s_.centrifugeId, POOL_A, address(s.syncManager).toBytes32(), true); - h.hub.updateBalanceSheetManager{value: GAS}(s_.centrifugeId, POOL_A, BSM.toBytes32(), true); - h.hub.setSnapshotHook(POOL_A, h.navManager); + h.hub.setSnapshotHook(POOL_A, h.snapshotHook); h.oracleValuation.updateFeeder(POOL_A, FEEDER, true); h.hub.updateHubManager(POOL_A, address(h.oracleValuation), true); vm.stopPrank(); @@ -559,11 +531,6 @@ contract EndToEndFlows is EndToEndUtils { IAsyncVault vault = _ensureAsyncVaultExists(hub, spoke, poolId, shareClassId, assetId, poolManager, existingVault); - uint16[] memory networks = new uint16[](1); - console2.log("Spoke network", spoke.centrifugeId); - networks[0] = spoke.centrifugeId; - h.priceManager.setNetworks(networks); - // Execute deposit request _executeAsyncDepositRequest(vault, investor, amount); @@ -1036,31 +1003,19 @@ contract EndToEndFlows is EndToEndUtils { function _testUpdateAccountingAfterDeposit(bool sameChain, bool afterAsyncDeposit, bool nonZeroPrices) public { (afterAsyncDeposit) ? _testAsyncDeposit(sameChain, nonZeroPrices) : _testSyncDeposit(sameChain, nonZeroPrices); - (uint128 amount1,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); - console2.log("Holding amount after deposit, before sync assets:", amount1); vm.startPrank(BSM); - console2.log("Submitting queued assets"); s.balanceSheet.submitQueuedAssets(POOL_A, SC_1, s.usdcId, EXTRA_GAS); - (uint128 amount2,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); - console2.log("Holding amount after deposit, after sync assets:", amount2); - console2.log("Submitting queued shares"); s.balanceSheet.submitQueuedShares(POOL_A, SC_1, EXTRA_GAS); - console2.log("Submitted"); - (uint128 amount3,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); - console2.log("Holding amount after deposit, after sync shares:", amount3); + // CHECKS (uint128 amount, uint128 value,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); - console2.log("Holding amount after deposit:", amount); - console2.log("Holding value after deposit:", value); assertEq(amount, USDC_AMOUNT_1, "expected amount"); assertEq(value, assetToPool(USDC_AMOUNT_1), "expected value"); - checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1), true); - checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1), true); + assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), nonZeroPrices ? 1 : 2, "expected snapshots"); - // (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); - // assertEq(issuance, ); - // assertEq(poolPerShare.raw(), d18(1, 1).raw()); + checkAccountValue(ASSET_ACCOUNT, assetToPool(USDC_AMOUNT_1), true); + checkAccountValue(EQUITY_ACCOUNT, assetToPool(USDC_AMOUNT_1), true); } function _testUpdateAccountingAfterRedeem(bool sameChain, bool afterAsyncDeposit) public { @@ -1074,12 +1029,10 @@ contract EndToEndFlows is EndToEndUtils { assertEq(amount, 0, "expected amount"); assertEq(value, assetToPool(0), "expected value"); - checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(0), true); - checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(0), true); + assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 2, "expected snapshots"); - // (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); - // assertEq(issuance, 0); - // assertEq(poolPerShare.raw(), d18(1, 1).raw()); + checkAccountValue(ASSET_ACCOUNT, assetToPool(0), true); + checkAccountValue(EQUITY_ACCOUNT, assetToPool(0), true); } } @@ -1170,6 +1123,7 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { h.hub.updateBalanceSheetManager{value: GAS}(s.centrifugeId, POOL_A, BSM.toBytes32(), true); h.hub.updateSharePrice(POOL_A, SC_1, IntegrationConstants.zeroPrice()); h.hub.notifySharePrice{value: GAS}(POOL_A, SC_1, s.centrifugeId); + h.hub.setSnapshotHook(POOL_A, h.snapshotHook); // Each message will return half of the gas wasted adapterBToA.setRefundedValue(h.gasService.updateShares() / 2); @@ -1193,6 +1147,8 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { s.balanceSheet.submitQueuedShares(POOL_A, SC_1, EXTRA_GAS); assertEq(address(s.balanceSheet.escrow(POOL_A)).balance, h.gasService.updateShares() / 2); assertEq(address(s.gateway).balance, 0); + + assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 3, "3 UpdateShares messages received"); } /// forge-config: default.isolate = true @@ -1212,9 +1168,8 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { } /// forge-config: default.isolate = true - function testFundManagement() public { - // bool sameChain - _configurePool(false); + function testFundManagement(bool sameChain) public { + _configurePool(sameChain); _configurePrices(IntegrationConstants.assetPrice(), IntegrationConstants.sharePrice()); vm.startPrank(ERC20_DEPLOYER); @@ -1223,35 +1178,21 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { vm.startPrank(BSM); s.usdc.approve(address(s.balanceSheet), USDC_AMOUNT_1); s.balanceSheet.deposit(POOL_A, SC_1, address(s.usdc), 0, USDC_AMOUNT_1); - - (uint128 amount1,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); - console2.log("Holding amount after deposit:", amount1); s.balanceSheet.withdraw(POOL_A, SC_1, address(s.usdc), 0, BSM, USDC_AMOUNT_1 * 4 / 5); - - (uint128 amount2,,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); - console2.log("Holding amount after withdraw:", amount2); - vm.startPrank(BSM); - console2.log("Submitting queued assets"); - s.balanceSheet.submitQueuedAssets(POOL_A, SC_1, s.usdcId, EXTRA_GAS); - console2.log("Submitted"); // CHECKS assertEq(s.usdc.balanceOf(BSM), USDC_AMOUNT_1 * 4 / 5); assertEq(s.balanceSheet.availableBalanceOf(POOL_A, SC_1, address(s.usdc), 0), USDC_AMOUNT_1 / 5); (uint128 amount, uint128 value,,) = h.holdings.holding(POOL_A, SC_1, s.usdcId); - console2.log("Holding amount after sync:", amount); - console2.log("Holding value after sync:", value); assertEq(amount, USDC_AMOUNT_1 / 5); assertEq(value, assetToPool(USDC_AMOUNT_1 / 5)); - checkAccountValue(h.navManager.assetAccount(s.centrifugeId, s.usdcId), assetToPool(USDC_AMOUNT_1 / 5), true); - checkAccountValue(h.navManager.equityAccount(s.centrifugeId), assetToPool(USDC_AMOUNT_1 / 5), true); + assertEq(h.snapshotHook.synced(POOL_A, SC_1, s.centrifugeId), 1); - (uint128 issuance, D18 poolPerShare) = h.shareClassManager.metrics(SC_1); - assertEq(issuance, 0); - assertEq(poolPerShare.raw(), d18(1, 1).raw()); + checkAccountValue(ASSET_ACCOUNT, assetToPool(USDC_AMOUNT_1 / 5), true); + checkAccountValue(EQUITY_ACCOUNT, assetToPool(USDC_AMOUNT_1 / 5), true); } /// forge-config: default.isolate = true @@ -1302,12 +1243,6 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { s.usdc.approve(address(vault), USDC_AMOUNT_1); vault.requestDeposit(USDC_AMOUNT_1, INVESTOR_A, INVESTOR_A); vault.cancelDepositRequest(PLACEHOLDER_REQUEST_ID, INVESTOR_A); - - (bool isCancelling, uint128 amount) = - h.shareClassManager.queuedDepositRequest(SC_1, s.usdcId, INVESTOR_A.toBytes32()); - console2.log("isCancelling", isCancelling); - console2.log("amount", amount); - vault.claimCancelDepositRequest(PLACEHOLDER_REQUEST_ID, INVESTOR_A, INVESTOR_A); // CHECKS @@ -1335,9 +1270,8 @@ contract EndToEndUseCases is EndToEndFlows, VMLabeling { } /// forge-config: default.isolate = true - function testUpdateAccountingAfterDeposit_AfterAsyncDeposit() public { - // TODO bool sameChain, bool nonZeroPrices - _testUpdateAccountingAfterDeposit(false, true, true); + function testUpdateAccountingAfterDeposit_AfterAsyncDeposit(bool sameChain, bool nonZeroPrices) public { + _testUpdateAccountingAfterDeposit(sameChain, true, nonZeroPrices); } /// forge-config: default.isolate = true diff --git a/test/integration/fork/ForkTestBase.sol b/test/integration/fork/ForkTestBase.sol index b0564fbe5..28f29d010 100644 --- a/test/integration/fork/ForkTestBase.sol +++ b/test/integration/fork/ForkTestBase.sol @@ -36,9 +36,6 @@ import {RedemptionRestrictions} from "../../../src/hooks/RedemptionRestrictions. import {OracleValuation} from "../../../src/valuations/OracleValuation.sol"; import {IdentityValuation} from "../../../src/valuations/IdentityValuation.sol"; -import {NAVManager} from "../../../src/managers/NAVManager.sol"; -import {SimplePriceManager} from "../../../src/managers/SimplePriceManager.sol"; - import "forge-std/Test.sol"; import {EndToEndFlows} from "../EndToEnd.t.sol"; @@ -79,8 +76,7 @@ contract ForkTestBase is EndToEndFlows { hub: Hub(IntegrationConstants.HUB), identityValuation: IdentityValuation(IntegrationConstants.IDENTITY_VALUATION), oracleValuation: OracleValuation(address(0)), // TODO: add this once deployed - navManager: NAVManager(address(0)), // Fork tests don't use snapshot hooks - priceManager: SimplePriceManager(payable(0)) // Fork tests doesn't use priceManager + snapshotHook: MockSnapshotHook(address(0)) // Fork tests don't use snapshot hooks }); forkSpoke = CSpoke({ diff --git a/test/integration/fork/ForkTestInvestments.sol b/test/integration/fork/ForkTestInvestments.sol index 16410a514..6bbfc2106 100644 --- a/test/integration/fork/ForkTestInvestments.sol +++ b/test/integration/fork/ForkTestInvestments.sol @@ -33,8 +33,7 @@ import {IBaseVault} from "../../../src/vaults/interfaces/IBaseVault.sol"; import {IAsyncVault} from "../../../src/vaults/interfaces/IAsyncVault.sol"; import {AsyncRequestManager} from "../../../src/vaults/AsyncRequestManager.sol"; -import {NAVManager} from "../../../src/managers/NAVManager.sol"; -import {SimplePriceManager} from "../../../src/managers/SimplePriceManager.sol"; +import {MockSnapshotHook} from "../../hooks/mocks/MockSnapshotHook.sol"; import {FreezeOnly} from "../../../src/hooks/FreezeOnly.sol"; import {FullRestrictions} from "../../../src/hooks/FullRestrictions.sol"; @@ -391,8 +390,7 @@ contract ForkTestSyncInvestments is ForkTestBase, VMLabeling { hub: Hub(IntegrationConstants.HUB), identityValuation: IdentityValuation(IntegrationConstants.IDENTITY_VALUATION), oracleValuation: OracleValuation(address(0)), // TODO: add this once deployed - navManager: NAVManager(address(0)), // Fork tests don't use snapshot hooks - priceManager: SimplePriceManager(payable(0)) // Fork tests doesn't use priceManager + snapshotHook: MockSnapshotHook(address(0)) // Fork tests don't use snapshot hooks }); forkSpoke = CSpoke({ From 95a1fb83a977118b3470861e641b4301f0ae357e Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:45:19 +0200 Subject: [PATCH 39/83] managers and callers --- script/HubDeployer.s.sol | 21 ++ src/managers/NAVManager.sol | 28 ++- src/managers/SimplePriceManager.sol | 112 ++++++++-- src/managers/interfaces/INAVManager.sol | 1 + .../interfaces/ISimplePriceManager.sol | 48 ++++- .../interfaces/ISimplePriceManagerFactory.sol | 15 ++ test/managers/unit/NAVManager.t.sol | 193 ++++++++++++++---- 7 files changed, 331 insertions(+), 87 deletions(-) create mode 100644 src/managers/interfaces/ISimplePriceManagerFactory.sol diff --git a/script/HubDeployer.s.sol b/script/HubDeployer.s.sol index 524e031cb..baee69d49 100644 --- a/script/HubDeployer.s.sol +++ b/script/HubDeployer.s.sol @@ -12,6 +12,9 @@ import {HubHelpers} from "../src/hub/HubHelpers.sol"; import {HubRegistry} from "../src/hub/HubRegistry.sol"; import {ShareClassManager} from "../src/hub/ShareClassManager.sol"; +import {NAVManagerFactory} from "../src/managers/NAVManager.sol"; +import {SimplePriceManagerFactory} from "../src/managers/SimplePriceManager.sol"; + import "forge-std/Script.sol"; abstract contract HubConstants { @@ -94,6 +97,8 @@ contract HubDeployer is CommonDeployer, HubConstants { ShareClassManager public shareClassManager; HubHelpers public hubHelpers; Hub public hub; + NAVManagerFactory public navManagerFactory; + SimplePriceManagerFactory public simplePriceManagerFactory; function deployHub(CommonInput memory input, HubActionBatcher batcher) public { _preDeployHub(input, batcher); @@ -160,6 +165,20 @@ contract HubDeployer is CommonDeployer, HubConstants { ) ); + navManagerFactory = NAVManagerFactory( + create3( + generateSalt("navManagerFactory"), + abi.encodePacked(type(NAVManagerFactory).creationCode, abi.encode(hub)) + ) + ); + + simplePriceManagerFactory = SimplePriceManagerFactory( + create3( + generateSalt("simplePriceManagerFactory"), + abi.encodePacked(type(SimplePriceManagerFactory).creationCode, abi.encode(hub)) + ) + ); + batcher.engageHub(_hubReport()); register("hubRegistry", address(hubRegistry)); @@ -168,6 +187,8 @@ contract HubDeployer is CommonDeployer, HubConstants { register("shareClassManager", address(shareClassManager)); register("hubHelpers", address(hubHelpers)); register("hub", address(hub)); + register("navManagerFactory", address(navManagerFactory)); + register("simplePriceManagerFactory", address(simplePriceManagerFactory)); } function _postDeployHub(HubActionBatcher batcher) internal { diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index c46c8cf6c..7def5ac8c 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -20,10 +20,11 @@ import {IHub} from "../hub/interfaces/IHub.sol"; import {IAccounting} from "../hub/interfaces/IAccounting.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. -contract NAVManager is Auth, INAVManager { +contract NAVManager is INAVManager { PoolId public immutable poolId; IHub public immutable hub; + IHubRegistry public immutable hubRegistry; address public immutable holdings; IAccounting public immutable accounting; @@ -32,17 +33,24 @@ contract NAVManager is Auth, INAVManager { mapping(uint16 centrifugeId => mapping(AssetId => AccountId)) public assetIdToAccountId; mapping(address => bool) public manager; - constructor(PoolId poolId_, IHub hub_, address deployer) Auth(deployer) { + constructor(PoolId poolId_, IHub hub_) { poolId = poolId_; hub = hub_; + hubRegistry = hub_.hubRegistry(); holdings = address(hub.holdings()); accounting = hub.accounting(); } - /// @dev Check if the msg.sender is ward or a manager + /// @dev Check if the msg.sender is a manager modifier onlyManager() { - require(wards[msg.sender] == 1 || manager[msg.sender], NotAuthorized()); + require(manager[msg.sender], NotAuthorized()); + _; + } + + /// @dev Check if the msg.sender is a hub manager + modifier onlyHubManager() { + require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); _; } @@ -51,13 +59,13 @@ contract NAVManager is Auth, INAVManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVManager - function setNAVHook(INAVHook navHook_) external auth { + function setNAVHook(INAVHook navHook_) external onlyHubManager { navHook = navHook_; emit SetNavHook(address(navHook_)); } /// @inheritdoc INAVManager - function updateManager(address manager_, bool canManage) external auth { + function updateManager(address manager_, bool canManage) external onlyHubManager { require(manager_ != address(0), EmptyAddress()); manager[manager_] = canManage; @@ -244,12 +252,10 @@ contract NAVManager is Auth, INAVManager { } } -contract NavManagerFactory is INAVManagerFactory { - address public immutable contractUpdater; +contract NAVManagerFactory is INAVManagerFactory { IHub public immutable hub; - constructor(address contractUpdater_, IHub hub_) { - contractUpdater = contractUpdater_; + constructor(IHub hub_) { hub = hub_; } @@ -257,7 +263,7 @@ contract NavManagerFactory is INAVManagerFactory { function newManager(PoolId poolId) external returns (INAVManager) { require(hub.hubRegistry().exists(poolId), InvalidPoolId()); - NAVManager manager = new NAVManager{salt: bytes32(uint256(poolId.raw()))}(poolId, hub, contractUpdater); + NAVManager manager = new NAVManager{salt: bytes32(uint256(poolId.raw()))}(poolId, hub); emit DeployNavManager(poolId, address(manager)); return INAVManager(manager); diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 57b351508..c4db6d7a1 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.28; import {console2} from "forge-std/console2.sol"; import {INAVHook} from "./interfaces/INavManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; +import {ISimplePriceManagerFactory} from "./interfaces/ISimplePriceManagerFactory.sol"; import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; @@ -15,50 +16,94 @@ import {ShareClassId} from "../common/types/ShareClassId.sol"; import {MAX_MESSAGE_COST} from "../common/interfaces/IGasService.sol"; import {IHub} from "../hub/interfaces/IHub.sol"; +import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; import {IShareClassManager} from "../hub/interfaces/IShareClassManager.sol"; /// @notice Share price calculation manager for single share class pools. -contract SimplePriceManager is Auth, ISimplePriceManager { +contract SimplePriceManager is ISimplePriceManager { PoolId public immutable poolId; ShareClassId public immutable scId; IHub public immutable hub; + IHubRegistry public immutable hubRegistry; IShareClassManager public immutable shareClassManager; uint16[] public networks; uint128 public globalIssuance; - D18 public globalNetAssetValue; + uint128 public globalNetAssetValue; mapping(uint16 centrifugeId => NetworkMetrics) public metrics; - constructor(PoolId poolId_, ShareClassId scId_, IHub hub_, address deployer) Auth(deployer) { + mapping(address => bool) public manager; + mapping(address => bool) public caller; + + constructor(PoolId poolId_, ShareClassId scId_, IHub hub_) { poolId = poolId_; scId = scId_; hub = hub_; + hubRegistry = hub_.hubRegistry(); shareClassManager = hub_.shareClassManager(); require(shareClassManager.shareClassCount(poolId_) == 1, InvalidShareClassCount()); } + /// @dev Check if the msg.sender is a manager + modifier onlyManager() { + require(manager[msg.sender], NotAuthorized()); + _; + } + + /// @dev Check if the msg.sender is a hub manager + modifier onlyHubManager() { + require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); + _; + } + + /// @dev Check if the msg.sender is a allowed caller + modifier onlyCaller() { + require(caller[msg.sender], NotAuthorized()); + _; + } + //---------------------------------------------------------------------------------------------- - // Network management + // Administration //---------------------------------------------------------------------------------------------- - /// @dev Ensure the number of network updates can fit in a single block - function setNetworks(uint16[] calldata centrifugeIds) external auth { + /// @inheritdoc ISimplePriceManager + function setNetworks(uint16[] calldata centrifugeIds) external onlyHubManager { networks = centrifugeIds; } + /// @inheritdoc ISimplePriceManager + function updateManager(address manager_, bool canManage) external onlyHubManager { + require(manager_ != address(0), EmptyAddress()); + + manager[manager_] = canManage; + + emit UpdateManager(manager_, canManage); + } + + /// @inheritdoc ISimplePriceManager + function updateCaller(address caller_, bool canCall) external onlyHubManager { + require(caller_ != address(0), EmptyAddress()); + + caller[caller_] = canCall; + + emit UpdateCaller(caller_, canCall); + } + //---------------------------------------------------------------------------------------------- - // Price updates + // Updates //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVHook - function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, D18 netAssetValue) external auth { - require(poolId == poolId_); - require(scId == scId_); - // TODO: check msg.sender - console2.log("SimplePriceManager onUpdate", netAssetValue.raw()); + function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, uint128 netAssetValue) + external + onlyCaller + { + require(poolId == poolId_, InvalidPoolId()); + require(scId == scId_, InvalidShareClassId()); + console2.log("SimplePriceManager onUpdate", netAssetValue); NetworkMetrics storage networkMetrics = metrics[centrifugeId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); @@ -69,7 +114,8 @@ contract SimplePriceManager is Auth, ISimplePriceManager { globalIssuance = globalIssuance + issuance - networkMetrics.issuance; globalNetAssetValue = globalNetAssetValue + netAssetValue - networkMetrics.netAssetValue; - D18 price = globalIssuance == 0 ? d18(1, 1) : globalNetAssetValue / d18(globalIssuance); + // TODO correct price calculation + D18 price = globalIssuance == 0 ? d18(1, 1) : d18(globalNetAssetValue) / d18(globalIssuance); console2.log("price", price.raw()); @@ -91,6 +137,8 @@ contract SimplePriceManager is Auth, ISimplePriceManager { IMulticall(address(hub)).multicall{value: MAX_MESSAGE_COST * (cs.length)}(cs); console2.log("SimplePriceManager onUpdate done"); + + emit Update(globalNetAssetValue, globalIssuance, price); } /// @inheritdoc INAVHook @@ -100,25 +148,26 @@ contract SimplePriceManager is Auth, ISimplePriceManager { uint16 fromCentrifugeId, uint16 toCentrifugeId, uint128 sharesTransferred - ) external auth { - require(poolId == poolId_); - require(scId == scId_); - // TODO check msg.sender + ) external onlyCaller { + require(poolId == poolId_, InvalidPoolId()); + require(scId == scId_, InvalidShareClassId()); NetworkMetrics storage fromMetrics = metrics[fromCentrifugeId]; NetworkMetrics storage toMetrics = metrics[toCentrifugeId]; fromMetrics.issuance -= sharesTransferred; toMetrics.issuance += sharesTransferred; + + emit Transfer(fromCentrifugeId, toCentrifugeId, sharesTransferred); } //---------------------------------------------------------------------------------------------- - // Investor actions + // Manager actions //---------------------------------------------------------------------------------------------- /// @inheritdoc ISimplePriceManager function approveDepositsAndIssueShares(AssetId depositAssetId, uint128 approvedAssetAmount, uint128 extraGasLimit) external - auth + onlyManager { uint32 nowDepositEpochId = shareClassManager.nowDepositEpoch(scId, depositAssetId); uint32 nowIssueEpochId = shareClassManager.nowIssueEpoch(scId, depositAssetId); @@ -126,6 +175,7 @@ contract SimplePriceManager is Auth, ISimplePriceManager { require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); D18 navPoolPerShare = _navPerShare(); + console2.log("navPoolPerShare", navPoolPerShare.raw()); hub.approveDeposits(poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount); hub.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); } @@ -133,7 +183,7 @@ contract SimplePriceManager is Auth, ISimplePriceManager { /// @inheritdoc ISimplePriceManager function approveRedeemsAndRevokeShares(AssetId payoutAssetId, uint128 approvedShareAmount, uint128 extraGasLimit) external - auth + onlyManager { uint32 nowRedeemEpochId = shareClassManager.nowRedeemEpoch(scId, payoutAssetId); uint32 nowRevokeEpochId = shareClassManager.nowRevokeEpoch(scId, payoutAssetId); @@ -150,10 +200,30 @@ contract SimplePriceManager is Auth, ISimplePriceManager { //---------------------------------------------------------------------------------------------- function _navPerShare() internal view returns (D18) { - return globalIssuance == 0 ? d18(1, 1) : globalNetAssetValue / d18(globalIssuance); + // TODO: Fix calc + return globalIssuance == 0 ? d18(1, 1) : d18(globalNetAssetValue) / d18(globalIssuance); } + // TODO: remove when not needed anymore receive() external payable { // Accept ETH refunds from multicall } } + +contract SimplePriceManagerFactory is ISimplePriceManagerFactory { + IHub public immutable hub; + + constructor(IHub hub_) { + hub = hub_; + } + + function newManager(PoolId poolId, ShareClassId scId) external returns (ISimplePriceManager) { + require(hub.shareClassManager().shareClassCount(poolId) == 1, InvalidShareClassCount()); + + SimplePriceManager manager = + new SimplePriceManager{salt: keccak256(abi.encode(poolId.raw(), scId.raw()))}(poolId, scId, hub); + + emit DeploySimplePriceManager(poolId, scId, address(manager)); + return ISimplePriceManager(manager); + } +} diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index 8a9ea175a..3e73e0040 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -53,6 +53,7 @@ interface INAVManager is ISnapshotHook { error InvalidNAVHook(); error InvalidPoolId(); error EmptyAddress(); + error NotAuthorized(); //---------------------------------------------------------------------------------------------- // Administration diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol index 1a7e7a58e..aa972314c 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -8,11 +8,20 @@ import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {INAVHook} from "./INavManager.sol"; interface ISimplePriceManager is INAVHook { + event Update(uint128 newNAV, uint128 newIssuance, D18 newSharePrice); + event Transfer(uint16 indexed fromCentrifugeId, uint16 indexed toCentrifugeId, uint128 sharesTransferred); + event UpdateManager(address indexed manager, bool canManage); + event UpdateCaller(address indexed caller, bool canCall); + error InvalidShareClassCount(); + error InvalidPoolId(); + error InvalidShareClassId(); error MismatchedEpochs(); + error EmptyAddress(); + error NotAuthorized(); struct NetworkMetrics { - D18 netAssetValue; + uint128 netAssetValue; uint128 issuance; } @@ -22,25 +31,42 @@ interface ISimplePriceManager is INAVHook { // function globalIssuance() external view returns (uint128); // function globalNetAssetValue() external view returns (D18); + //---------------------------------------------------------------------------------------------- + // Administration + //---------------------------------------------------------------------------------------------- + + /// @notice Update the list of networks the pool is active on + /// @dev Ensure the number of network updates can fit in a single block function setNetworks(uint16[] calldata centrifugeIds) external; + /// @notice Check if an address can manage the NAV manager + function manager(address manager) external view returns (bool); + + /// @notice Update whether an address can manage the NAV manager + /// @param manager The address of the manager + /// @param canManage Whether the address can manage this manager + function updateManager(address manager, bool canManage) external; + + /// @notice Update whether an address can call NAVHook methods + /// @param caller The address of the caller + /// @param canCall Whether the address can call NAVHook methods + function updateCaller(address caller, bool canCall) external; + + //---------------------------------------------------------------------------------------------- + // Manager actions + //---------------------------------------------------------------------------------------------- + /// @notice Approve deposits and issue shares in sequence using current NAV per share /// @param depositAssetId The asset ID for deposits /// @param approvedAssetAmount Amount of assets to approve /// @param extraGasLimit Extra gas limit for cross-chain operations - function approveDepositsAndIssueShares( - AssetId depositAssetId, - uint128 approvedAssetAmount, - uint128 extraGasLimit - ) external; + function approveDepositsAndIssueShares(AssetId depositAssetId, uint128 approvedAssetAmount, uint128 extraGasLimit) + external; /// @notice Approve redeems and revoke shares in sequence using current NAV per share /// @param payoutAssetId The asset ID for payouts /// @param approvedShareAmount Amount of shares to approve for redemption /// @param extraGasLimit Extra gas limit for cross-chain operations - function approveRedeemsAndRevokeShares( - AssetId payoutAssetId, - uint128 approvedShareAmount, - uint128 extraGasLimit - ) external; + function approveRedeemsAndRevokeShares(AssetId payoutAssetId, uint128 approvedShareAmount, uint128 extraGasLimit) + external; } diff --git a/src/managers/interfaces/ISimplePriceManagerFactory.sol b/src/managers/interfaces/ISimplePriceManagerFactory.sol new file mode 100644 index 000000000..bd36155b7 --- /dev/null +++ b/src/managers/interfaces/ISimplePriceManagerFactory.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.28; + +import {ISimplePriceManager} from "./ISimplePriceManager.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; + +interface ISimplePriceManagerFactory { + event DeploySimplePriceManager(PoolId indexed poolId, ShareClassId indexed scId, address indexed manager); + + error InvalidShareClassCount(); + + function newManager(PoolId poolId, ShareClassId scId) external returns (ISimplePriceManager); +} diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/unit/NAVManager.t.sol index 3c0cf3247..66fdd87cd 100644 --- a/test/managers/unit/NAVManager.t.sol +++ b/test/managers/unit/NAVManager.t.sol @@ -10,7 +10,7 @@ import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; -import {NAVManager, NavManagerFactory} from "../../../src/managers/NAVManager.sol"; +import {NAVManager, NAVManagerFactory} from "../../../src/managers/NAVManager.sol"; import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; import {INAVManagerFactory} from "../../../src/managers/interfaces/INAVManagerFactory.sol"; @@ -23,7 +23,6 @@ import {Mock} from "../../common/mocks/Mock.sol"; import "forge-std/Test.sol"; -// Mock contracts to bypass foundry mockCall issues contract IsContract {} contract NAVManagerTest is Test { @@ -43,10 +42,9 @@ contract NAVManagerTest is Test { address hubRegistry = address(new IsContract()); INAVHook navHook = INAVHook(address(new IsContract())); - address deployer = address(this); - address contractUpdater = makeAddr("contractUpdater"); address unauthorized = makeAddr("unauthorized"); address manager = makeAddr("manager"); + address hubManager = makeAddr("hubManager"); NAVManager navManager; MockValuation mockValuation; @@ -75,14 +73,19 @@ contract NAVManagerTest is Test { vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint128)", asset1), abi.encode(6)); vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint128)", asset2), abi.encode(6)); vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint64)", POOL_A), abi.encode(18)); + vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); + vm.mockCall( + hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_A, hubManager), abi.encode(true) + ); vm.mockCall(address(navHook), abi.encodeWithSelector(INAVHook.onUpdate.selector), abi.encode()); vm.mockCall(address(navHook), abi.encodeWithSelector(INAVHook.onTransfer.selector), abi.encode()); } function _deployManager() internal { - vm.prank(deployer); - navManager = new NAVManager(POOL_A, IHub(hub), deployer); + navManager = new NAVManager(POOL_A, IHub(hub)); + vm.prank(hubManager); + navManager.updateManager(manager, true); } function _mockAccountValue(AccountId accountId, uint128 value, bool isPositive) internal { @@ -96,7 +99,7 @@ contract NAVManagerTest is Test { contract NAVManagerConstructorTest is NAVManagerTest { function testConstructor() public view { - assertEq(PoolId.unwrap(navManager.poolId()), PoolId.unwrap(POOL_A)); + assertEq(navManager.poolId().raw(), POOL_A.raw()); assertEq(address(navManager.hub()), address(hub)); assertEq(navManager.holdings(), holdings); assertEq(address(navManager.accounting()), address(accounting)); @@ -106,7 +109,10 @@ contract NAVManagerConstructorTest is NAVManagerTest { contract NAVManagerConfigureTest is NAVManagerTest { function testSetNAVHookSuccess() public { - vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit INAVManager.SetNavHook(address(navHook)); + + vm.prank(hubManager); navManager.setNAVHook(navHook); assertEq(address(navManager.navHook()), address(navHook)); @@ -119,12 +125,54 @@ contract NAVManagerConfigureTest is NAVManagerTest { } function testSetNAVHookToZeroAddress() public { - vm.prank(deployer); + vm.prank(hubManager); navManager.setNAVHook(INAVHook(address(0))); assertEq(address(navManager.navHook()), address(0)); } + function testUpdateManagerSuccess() public { + address newManager = makeAddr("newManager"); + + vm.expectEmit(true, true, false, false); + emit INAVManager.UpdateManager(newManager, true); + + vm.prank(hubManager); + navManager.updateManager(newManager, true); + + assertTrue(navManager.manager(newManager)); + } + + function testUpdateManagerRemove() public { + address managerAddr = makeAddr("newManager"); + + vm.prank(hubManager); + navManager.updateManager(managerAddr, true); + assertTrue(navManager.manager(managerAddr)); + + vm.expectEmit(true, true, false, false); + emit INAVManager.UpdateManager(managerAddr, false); + + vm.prank(hubManager); + navManager.updateManager(managerAddr, false); + + assertFalse(navManager.manager(managerAddr)); + } + + function testUpdateManagerUnauthorized() public { + address managerAddr = makeAddr("newManager"); + + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + navManager.updateManager(managerAddr, true); + } + + function testUpdateManagerZeroAddress() public { + vm.expectRevert(INAVManager.EmptyAddress.selector); + vm.prank(hubManager); + navManager.updateManager(address(0), true); + } + function testInitializeNetworkSuccess() public { vm.expectCall( address(hub), @@ -147,15 +195,27 @@ contract NAVManagerConfigureTest is NAVManagerTest { abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, navManager.lossAccount(CENTRIFUGE_ID_1), false) ); + vm.expectEmit(true, false, false, true); + emit INAVManager.InitializeNetwork(CENTRIFUGE_ID_1); + + vm.prank(manager); navManager.initializeNetwork(CENTRIFUGE_ID_1); assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 5); } function testInitializeNetworkAlreadyInitialized() public { + vm.prank(manager); navManager.initializeNetwork(CENTRIFUGE_ID_1); vm.expectRevert(INAVManager.AlreadyInitialized.selector); + vm.prank(manager); + navManager.initializeNetwork(CENTRIFUGE_ID_1); + } + + function testInitializeNetworkUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); navManager.initializeNetwork(CENTRIFUGE_ID_1); } } @@ -163,7 +223,7 @@ contract NAVManagerConfigureTest is NAVManagerTest { contract NAVManagerHoldingInitializationTest is NAVManagerTest { function setUp() public override { super.setUp(); - // Initialize network first + vm.prank(manager); navManager.initializeNetwork(CENTRIFUGE_ID_1); } @@ -188,39 +248,50 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { ) ); + vm.expectEmit(true, true, false, true); + emit INAVManager.InitializeHolding(SC_1, asset1); + + vm.prank(manager); navManager.initializeHolding(SC_1, asset1, mockValuation); assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 6); - assertEq( - AccountId.unwrap(navManager.assetAccount(CENTRIFUGE_ID_1, asset1)), AccountId.unwrap(expectedAssetAccount) - ); + assertEq(navManager.assetAccount(CENTRIFUGE_ID_1, asset1).raw(), expectedAssetAccount.raw()); } function testInitializeHoldingNotInitialized() public { vm.expectRevert(INAVManager.NotInitialized.selector); + vm.prank(manager); navManager.initializeHolding(SC_1, AssetId.wrap(uint128(3) << 64 | 300), mockValuation); } function testInitializeHoldingSameAssetTwice() public { + vm.prank(manager); navManager.initializeHolding(SC_1, asset1, mockValuation); - // Should reuse the same account AccountId expectedAssetAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); vm.expectCall( address(hub), abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, expectedAssetAccount, true) ); + vm.prank(manager); navManager.initializeHolding(SC_2, asset1, mockValuation); // Account counter should increment again assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 7); } + + function testInitializeHoldingUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + navManager.initializeHolding(SC_1, asset1, mockValuation); + } } contract NAVManagerLiabilityInitializationTest is NAVManagerTest { function setUp() public override { super.setUp(); + vm.prank(manager); navManager.initializeNetwork(CENTRIFUGE_ID_1); } @@ -243,26 +314,36 @@ contract NAVManagerLiabilityInitializationTest is NAVManagerTest { ) ); + vm.expectEmit(true, true, false, true); + emit INAVManager.InitializeLiability(SC_1, asset1); + + vm.prank(manager); navManager.initializeLiability(SC_1, asset1, mockValuation); assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 6); - assertEq( - AccountId.unwrap(navManager.expenseAccount(CENTRIFUGE_ID_1, asset1)), - AccountId.unwrap(expectedExpenseAccount) - ); + assertEq(navManager.expenseAccount(CENTRIFUGE_ID_1, asset1).raw(), expectedExpenseAccount.raw()); } function testInitializeLiabilityNotInitialized() public { vm.expectRevert(INAVManager.NotInitialized.selector); + vm.prank(manager); navManager.initializeLiability(SC_1, asset2, mockValuation); } + + function testInitializeLiabilityUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + navManager.initializeLiability(SC_1, asset1, mockValuation); + } } contract NAVManagerOnSyncTest is NAVManagerTest { function setUp() public override { super.setUp(); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + vm.prank(hubManager); navManager.setNAVHook(navHook); + vm.prank(manager); + navManager.initializeNetwork(CENTRIFUGE_ID_1); } function testOnSyncSuccess() public { @@ -274,18 +355,14 @@ contract NAVManagerOnSyncTest is NAVManagerTest { _mockAccountValue(navManager.liabilityAccount(CENTRIFUGE_ID_1), 50, true); vm.expectCall( - address(navHook), - abi.encodeWithSelector(INAVHook.onUpdate.selector, POOL_A, SC_1, CENTRIFUGE_ID_1, d18(1050)) + address(navHook), abi.encodeWithSelector(INAVHook.onUpdate.selector, POOL_A, SC_1, CENTRIFUGE_ID_1, 1050) ); + vm.expectEmit(true, true, false, true); + emit INAVManager.Sync(SC_1, CENTRIFUGE_ID_1, 1050); + vm.prank(holdings); navManager.onSync(POOL_A, SC_1, CENTRIFUGE_ID_1); - - // assertEq(mockNAVHook.updateCallCount(), 1); - // assertEq(PoolId.unwrap(mockNAVHook.lastPoolId()), PoolId.unwrap(POOL_A)); - // assertEq(ShareClassId.unwrap(mockNAVHook.lastScId()), ShareClassId.unwrap(SC_1)); - // assertEq(mockNAVHook.lastCentrifugeId(), CENTRIFUGE_ID_1); - // assertEq(mockNAVHook.lastNetAssetValue().raw(), d18(1050).raw()); } function testOnSyncInvalidPoolId() public { @@ -302,7 +379,7 @@ contract NAVManagerOnSyncTest is NAVManagerTest { function testOnSyncNoNAVHook() public { // Reset NAV hook to zero - vm.prank(deployer); + vm.prank(hubManager); navManager.setNAVHook(INAVHook(address(0))); vm.expectRevert(INAVManager.InvalidNAVHook.selector); @@ -347,6 +424,13 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { function testUpdateHoldingValue() public { vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateHoldingValue.selector, POOL_A, SC_1, asset1)); + vm.prank(manager); + navManager.updateHoldingValue(SC_1, asset1); + } + + function testUpdateHoldingValueUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); navManager.updateHoldingValue(SC_1, asset1); } @@ -356,6 +440,13 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { abi.encodeWithSelector(IHub.updateHoldingValuation.selector, POOL_A, SC_1, asset1, mockValuation) ); + vm.prank(manager); + navManager.updateHoldingValuation(SC_1, asset1, mockValuation); + } + + function testUpdateHoldingValuationUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); navManager.updateHoldingValuation(SC_1, asset1, mockValuation); } @@ -368,6 +459,16 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { abi.encodeWithSelector(IHub.setHoldingAccountId.selector, POOL_A, SC_1, asset1, kind, accountId) ); + vm.prank(manager); + navManager.setHoldingAccountId(SC_1, asset1, kind, accountId); + } + + function testSetHoldingAccountIdUnauthorized() public { + AccountId accountId = withCentrifugeId(CENTRIFUGE_ID_1, 10); + uint8 kind = 1; + + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); navManager.setHoldingAccountId(SC_1, asset1, kind, accountId); } } @@ -376,43 +477,47 @@ contract NAVManagerHelperFunctionsTest is NAVManagerTest { function testEquityAccount() public view { AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 1); AccountId actual = navManager.equityAccount(CENTRIFUGE_ID_1); - assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + assertEq(actual.raw(), expected.raw()); } function testLiabilityAccount() public view { AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 2); AccountId actual = navManager.liabilityAccount(CENTRIFUGE_ID_1); - assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + assertEq(actual.raw(), expected.raw()); } function testGainAccount() public view { AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 3); AccountId actual = navManager.gainAccount(CENTRIFUGE_ID_1); - assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + assertEq(actual.raw(), expected.raw()); } function testLossAccount() public view { AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 4); AccountId actual = navManager.lossAccount(CENTRIFUGE_ID_1); - assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + assertEq(actual.raw(), expected.raw()); } function testAssetAccount() public { + vm.prank(manager); navManager.initializeNetwork(CENTRIFUGE_ID_1); + vm.prank(manager); navManager.initializeHolding(SC_1, asset1, mockValuation); AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); AccountId actual = navManager.assetAccount(CENTRIFUGE_ID_1, asset1); - assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + assertEq(actual.raw(), expected.raw()); } function testExpenseAccount() public { + vm.prank(manager); navManager.initializeNetwork(CENTRIFUGE_ID_1); + vm.prank(manager); navManager.initializeLiability(SC_1, asset1, mockValuation); AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); AccountId actual = navManager.expenseAccount(CENTRIFUGE_ID_1, asset1); - assertEq(AccountId.unwrap(actual), AccountId.unwrap(expected)); + assertEq(actual.raw(), expected.raw()); } function testAssetAccountNotInitialized() public view { @@ -424,6 +529,7 @@ contract NAVManagerHelperFunctionsTest is NAVManagerTest { contract NAVManagerOnTransferTest is NAVManagerTest { function setUp() public override { super.setUp(); + vm.prank(hubManager); navManager.setNAVHook(navHook); } @@ -440,7 +546,7 @@ contract NAVManagerOnTransferTest is NAVManagerTest { } function testOnTransferNoNAVHook() public { - vm.prank(deployer); + vm.prank(hubManager); navManager.setNAVHook(INAVHook(address(0))); vm.expectRevert(INAVManager.InvalidNAVHook.selector); @@ -451,9 +557,12 @@ contract NAVManagerOnTransferTest is NAVManagerTest { function testOnTransferSuccess() public { vm.expectCall( address(navHook), - abi.encodeWithSelector(INAVHook.onTransfer.selector, POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, d18(1)) + abi.encodeWithSelector(INAVHook.onTransfer.selector, POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1) ); + vm.expectEmit(true, true, true, true); + emit INAVManager.Transfer(SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); + vm.prank(hub); navManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); } @@ -464,43 +573,39 @@ contract NAVManagerFactoryTest is Test { address accounting = address(new IsContract()); address holdings = address(new IsContract()); address hubRegistry = address(new IsContract()); - address contractUpdater = makeAddr("contractUpdater"); - NavManagerFactory factory; + NAVManagerFactory factory; function setUp() public { vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); vm.mockCall(hub, abi.encodeWithSelector(IHub.accounting.selector), abi.encode(accounting)); vm.mockCall(hub, abi.encodeWithSelector(IHub.holdings.selector), abi.encode(holdings)); - factory = new NavManagerFactory(contractUpdater, IHub(hub)); + factory = new NAVManagerFactory(IHub(hub)); } function testFactoryConstructor() public view { - assertEq(factory.contractUpdater(), contractUpdater); assertEq(address(factory.hub()), address(hub)); } function testNewManagerSuccess() public { PoolId poolId = PoolId.wrap(1); - // Mock hubRegistry.exists() to return true vm.mockCall( address(hubRegistry), abi.encodeWithSelector(IHubRegistry.exists.selector, poolId), abi.encode(true) ); vm.expectEmit(true, false, false, false); - emit INAVManagerFactory.DeployNavManager(poolId, address(0)); // address will be different + emit INAVManagerFactory.DeployNavManager(poolId, address(0)); INAVManager manager = factory.newManager(poolId); assertTrue(address(manager) != address(0)); - assertEq(PoolId.unwrap(NAVManager(address(manager)).poolId()), PoolId.unwrap(poolId)); + assertEq(NAVManager(address(manager)).poolId().raw(), poolId.raw()); } function testNewManagerInvalidPool() public { PoolId poolId = PoolId.wrap(1); - // Mock hubRegistry.exists() to return false vm.mockCall( address(hubRegistry), abi.encodeWithSelector(IHubRegistry.exists.selector, poolId), abi.encode(false) ); From f328ab4918ce733574d59919771fd11826e58798 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:01:25 +0200 Subject: [PATCH 40/83] fix e2e --- test/integration/ThreeChainEndToEnd.t.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/integration/ThreeChainEndToEnd.t.sol b/test/integration/ThreeChainEndToEnd.t.sol index bf3be2190..48866a5a0 100644 --- a/test/integration/ThreeChainEndToEnd.t.sol +++ b/test/integration/ThreeChainEndToEnd.t.sol @@ -6,6 +6,7 @@ import {LocalAdapter} from "./adapters/LocalAdapter.sol"; import {IntegrationConstants} from "./utils/IntegrationConstants.sol"; import {CastLib} from "../../src/misc/libraries/CastLib.sol"; +import {d18} from "../../src/misc/types/D18.sol"; import {PoolId} from "../../src/common/types/PoolId.sol"; import {ISafe} from "../../src/common/interfaces/IGuardian.sol"; @@ -101,10 +102,15 @@ contract ThreeChainEndToEndDeployment is EndToEndUseCases { _testConfigurePool(direction); + vm.startPrank(FM); + h.hub.updateSharePrice(POOL_A, SC_1, d18(1, 1)); + h.hub.notifySharePrice{value: GAS}(POOL_A, SC_1, sB.centrifugeId); + // B: Mint shares - vm.startPrank(address(sB.root)); + vm.startPrank(BSM); IShareToken shareTokenB = IShareToken(sB.spoke.shareToken(POOL_A, SC_1)); - shareTokenB.mint(INVESTOR_A, amount); + sB.balanceSheet.issue(POOL_A, SC_1, INVESTOR_A, amount); + sB.balanceSheet.submitQueuedShares(POOL_A, SC_1, 0); vm.stopPrank(); assertEq(shareTokenB.balanceOf(INVESTOR_A), amount, "Investor should have minted shares on chain B"); From ff9089a88e39ebcd2e148315dfab598b6e207be5 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:29:38 +0200 Subject: [PATCH 41/83] simplepricemanager test --- src/hub/interfaces/IHoldings.sol | 6 + src/managers/NAVManager.sol | 56 +- src/managers/SimplePriceManager.sol | 5 +- src/managers/interfaces/INAVManager.sol | 15 +- .../interfaces/ISimplePriceManager.sol | 8 +- test/managers/unit/NAVManager.t.sol | 14 +- test/managers/unit/SimplePriceManager.t.sol | 589 ++++++++++++++++++ 7 files changed, 647 insertions(+), 46 deletions(-) create mode 100644 test/managers/unit/SimplePriceManager.t.sol diff --git a/src/hub/interfaces/IHoldings.sol b/src/hub/interfaces/IHoldings.sol index a5499b0cd..ffdd5a010 100644 --- a/src/hub/interfaces/IHoldings.sol +++ b/src/hub/interfaces/IHoldings.sol @@ -156,6 +156,12 @@ interface IHoldings { /// @notice Returns the snapshot hook for the given pool. function snapshotHook(PoolId poolId) external view returns (ISnapshotHook); + /// @notice Returns the snapshot info for a given pool, share class and centrifugeId. + function snapshot(PoolId poolId, ShareClassId scId, uint16 centrifugeId) + external + view + returns (bool isSnapshot, uint64 nonce); + /// @notice Returns the value of this holding. function value(PoolId poolId, ShareClassId scId, AssetId assetId) external view returns (uint128 value); diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 7def5ac8c..b8f4d7973 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -14,6 +14,7 @@ import {ShareClassId} from "../common/types/ShareClassId.sol"; import {IValuation} from "../common/interfaces/IValuation.sol"; import {ISnapshotHook} from "../common/interfaces/ISnapshotHook.sol"; import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; +import {IHoldings} from "../hub/interfaces/IHoldings.sol"; import {AccountId, withCentrifugeId} from "../common/types/AccountId.sol"; import {IHub} from "../hub/interfaces/IHub.sol"; @@ -25,7 +26,7 @@ contract NAVManager is INAVManager { IHub public immutable hub; IHubRegistry public immutable hubRegistry; - address public immutable holdings; + IHoldings public immutable holdings; IAccounting public immutable accounting; INAVHook public navHook; @@ -38,7 +39,7 @@ contract NAVManager is INAVManager { hub = hub_; hubRegistry = hub_.hubRegistry(); - holdings = address(hub.holdings()); + holdings = hub.holdings(); accounting = hub.accounting(); } @@ -149,16 +150,9 @@ contract NAVManager is INAVManager { /// @inheritdoc ISnapshotHook function onSync(PoolId poolId_, ShareClassId scId, uint16 centrifugeId) external { console2.log("NAVManager onSync"); - require(msg.sender == holdings, NotAuthorized()); + require(msg.sender == address(holdings), NotAuthorized()); require(poolId == poolId_, InvalidPoolId()); - require(address(navHook) != address(0), InvalidNAVHook()); - - uint128 netAssetValue_ = netAssetValue(centrifugeId); - console2.log("NAV", netAssetValue_); - navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); - console2.log("NAVManager onSync done"); - - emit Sync(scId, centrifugeId, netAssetValue_); + _onSync(scId, centrifugeId); } /// @inheritdoc ISnapshotHook @@ -179,13 +173,18 @@ contract NAVManager is INAVManager { } /// @inheritdoc INAVManager - function updateHoldingValue(ShareClassId scId, AssetId assetId) external onlyManager { + function updateHoldingValue(ShareClassId scId, AssetId assetId) public onlyManager { hub.updateHoldingValue(poolId, scId, assetId); + (bool isSnapshot,) = holdings.snapshot(poolId, scId, assetId.centrifugeId()); + if (isSnapshot) { + _onSync(scId, assetId.centrifugeId()); + } } /// @inheritdoc INAVManager function updateHoldingValuation(ShareClassId scId, AssetId assetId, IValuation valuation) external onlyManager { hub.updateHoldingValuation(poolId, scId, assetId, valuation); + updateHoldingValue(scId, assetId); } /// @inheritdoc INAVManager @@ -204,16 +203,14 @@ contract NAVManager is INAVManager { /// @inheritdoc INAVManager function netAssetValue(uint16 centrifugeId) public view returns (uint128) { - // TODO: how to handle when one of the accounts is not positive - (, uint128 equity) = accounting.accountValue(poolId, equityAccount(centrifugeId)); - (, uint128 gain) = accounting.accountValue(poolId, gainAccount(centrifugeId)); - (, uint128 loss) = accounting.accountValue(poolId, lossAccount(centrifugeId)); - (, uint128 liability) = accounting.accountValue(poolId, liabilityAccount(centrifugeId)); - - console2.log("Equity", equity); - console2.log("Gain", gain); - console2.log("Loss", loss); - console2.log("Liability", liability); + // TODO: how to handle when one of the accounts is not positive (or positive for loss account) + (bool equityIsPositive, uint128 equity) = accounting.accountValue(poolId, equityAccount(centrifugeId)); + (bool gainIsPositive, uint128 gain) = accounting.accountValue(poolId, gainAccount(centrifugeId)); + (bool lossIsPositive, uint128 loss) = accounting.accountValue(poolId, lossAccount(centrifugeId)); + (bool liabilityIsPositive, uint128 liability) = accounting.accountValue(poolId, liabilityAccount(centrifugeId)); + + require(equityIsPositive && gainIsPositive && liabilityIsPositive && (!lossIsPositive || loss == 0), ""); + return equity + gain - loss - liability; } @@ -250,6 +247,21 @@ contract NAVManager is INAVManager { function lossAccount(uint16 centrifugeId) public pure returns (AccountId) { return withCentrifugeId(centrifugeId, 4); } + + //---------------------------------------------------------------------------------------------- + // Internal methods + //---------------------------------------------------------------------------------------------- + + function _onSync(ShareClassId scId, uint16 centrifugeId) internal { + require(address(navHook) != address(0), InvalidNAVHook()); + + uint128 netAssetValue_ = netAssetValue(centrifugeId); + console2.log("NAV", netAssetValue_); + navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); + console2.log("NAVManager onSync done"); + + emit Sync(scId, centrifugeId, netAssetValue_); + } } contract NAVManagerFactory is INAVManagerFactory { diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index c4db6d7a1..b6874a89f 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -114,8 +114,7 @@ contract SimplePriceManager is ISimplePriceManager { globalIssuance = globalIssuance + issuance - networkMetrics.issuance; globalNetAssetValue = globalNetAssetValue + netAssetValue - networkMetrics.netAssetValue; - // TODO correct price calculation - D18 price = globalIssuance == 0 ? d18(1, 1) : d18(globalNetAssetValue) / d18(globalIssuance); + D18 price = _navPerShare(); console2.log("price", price.raw()); @@ -200,7 +199,6 @@ contract SimplePriceManager is ISimplePriceManager { //---------------------------------------------------------------------------------------------- function _navPerShare() internal view returns (D18) { - // TODO: Fix calc return globalIssuance == 0 ? d18(1, 1) : d18(globalNetAssetValue) / d18(globalIssuance); } @@ -217,6 +215,7 @@ contract SimplePriceManagerFactory is ISimplePriceManagerFactory { hub = hub_; } + // TODO: remove scId param function newManager(PoolId poolId, ShareClassId scId) external returns (ISimplePriceManager) { require(hub.shareClassManager().shareClassCount(poolId) == 1, InvalidShareClassCount()); diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index 3e73e0040..3b4877a87 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -66,12 +66,12 @@ interface INAVManager is ISnapshotHook { /// @param navHook The address of the NAV hook contract function setNAVHook(INAVHook navHook) external; - /// @notice Check if an address can manage the NAV manager + /// @notice Check if an address can call management functions function manager(address manager) external view returns (bool); - /// @notice Update whether an address can manage the NAV manager + /// @notice Update whether an address can call management functions /// @param manager The address of the manager - /// @param canManage Whether the address can manage this manager + /// @param canManage Whether the address can call management functions function updateManager(address manager, bool canManage) external; //---------------------------------------------------------------------------------------------- @@ -112,7 +112,7 @@ interface INAVManager is ISnapshotHook { /// @notice Set the account ID for a specific asset holding /// @param scId The share class ID /// @param assetId The asset ID - /// @param kind The account kind (type) + /// @param kind The account kind /// @param accountId The account ID to set function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) external; @@ -123,7 +123,6 @@ interface INAVManager is ISnapshotHook { /// @notice Calculate the net asset value for a specific network /// @dev NAV = equity + gain - loss - liability /// @param centrifugeId The Centrifuge ID of the network - /// @return The calculated net asset value function netAssetValue(uint16 centrifugeId) external view returns (uint128); //---------------------------------------------------------------------------------------------- @@ -133,32 +132,26 @@ interface INAVManager is ISnapshotHook { /// @notice Get the asset account ID for a specific asset on a network /// @param centrifugeId The Centrifuge ID of the network /// @param assetId The asset ID - /// @return The account ID for the asset function assetAccount(uint16 centrifugeId, AssetId assetId) external view returns (AccountId); /// @notice Get the expense account ID for a specific asset on a network /// @param centrifugeId The Centrifuge ID of the network /// @param assetId The asset ID - /// @return The account ID for the expense function expenseAccount(uint16 centrifugeId, AssetId assetId) external view returns (AccountId); /// @notice Get the equity account ID for a specific network /// @param centrifugeId The Centrifuge ID of the network - /// @return The equity account ID function equityAccount(uint16 centrifugeId) external pure returns (AccountId); /// @notice Get the liability account ID for a specific network /// @param centrifugeId The Centrifuge ID of the network - /// @return The liability account ID function liabilityAccount(uint16 centrifugeId) external pure returns (AccountId); /// @notice Get the gain account ID for a specific network /// @param centrifugeId The Centrifuge ID of the network - /// @return The gain account ID function gainAccount(uint16 centrifugeId) external pure returns (AccountId); /// @notice Get the loss account ID for a specific network /// @param centrifugeId The Centrifuge ID of the network - /// @return The loss account ID function lossAccount(uint16 centrifugeId) external pure returns (AccountId); } diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol index aa972314c..2f21171d5 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -25,11 +25,9 @@ interface ISimplePriceManager is INAVHook { uint128 issuance; } - // function poolId() external view returns (PoolId); - // function scId() external view returns (ShareClassId); - // function networks(uint256 index) external view returns (uint16); - // function globalIssuance() external view returns (uint128); - // function globalNetAssetValue() external view returns (D18); + function globalIssuance() external view returns (uint128); + function globalNetAssetValue() external view returns (uint128); + function metrics(uint16 centrifugeId) external view returns (uint128 netAssetValue, uint128 issuance); //---------------------------------------------------------------------------------------------- // Administration diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/unit/NAVManager.t.sol index 66fdd87cd..e999043b9 100644 --- a/test/managers/unit/NAVManager.t.sol +++ b/test/managers/unit/NAVManager.t.sol @@ -15,6 +15,7 @@ import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManage import {INAVManagerFactory} from "../../../src/managers/interfaces/INAVManagerFactory.sol"; import {IHub} from "../../../src/hub/interfaces/IHub.sol"; +import {IHoldings} from "../../../src/hub/interfaces/IHoldings.sol"; import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; @@ -68,6 +69,8 @@ contract NAVManagerTest is Test { vm.mockCall(hub, abi.encodeWithSelector(IHub.updateHoldingValuation.selector), abi.encode()); vm.mockCall(hub, abi.encodeWithSelector(IHub.setHoldingAccountId.selector), abi.encode()); + vm.mockCall(holdings, abi.encodeWithSelector(IHoldings.snapshot.selector), abi.encode(false, uint64(0))); + vm.mockCall(accounting, abi.encodeWithSelector(IAccounting.accountValue.selector), abi.encode(true, uint128(0))); vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint128)", asset1), abi.encode(6)); @@ -101,7 +104,7 @@ contract NAVManagerConstructorTest is NAVManagerTest { function testConstructor() public view { assertEq(navManager.poolId().raw(), POOL_A.raw()); assertEq(address(navManager.hub()), address(hub)); - assertEq(navManager.holdings(), holdings); + assertEq(address(navManager.holdings()), holdings); assertEq(address(navManager.accounting()), address(accounting)); assertEq(address(navManager.navHook()), address(0)); } @@ -366,7 +369,7 @@ contract NAVManagerOnSyncTest is NAVManagerTest { } function testOnSyncInvalidPoolId() public { - vm.expectRevert(); + vm.expectRevert(INAVManager.InvalidPoolId.selector); vm.prank(holdings); navManager.onSync(POOL_B, SC_1, CENTRIFUGE_ID_1); } @@ -416,7 +419,7 @@ contract NAVManagerNetAssetValueTest is NAVManagerTest { _mockAccountValue(navManager.liabilityAccount(CENTRIFUGE_ID_1), 100, true); vm.expectRevert(); - uint128 nav = navManager.netAssetValue(CENTRIFUGE_ID_1); + navManager.netAssetValue(CENTRIFUGE_ID_1); } } @@ -439,6 +442,7 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { address(hub), abi.encodeWithSelector(IHub.updateHoldingValuation.selector, POOL_A, SC_1, asset1, mockValuation) ); + vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateHoldingValue.selector, POOL_A, SC_1, asset1)); vm.prank(manager); navManager.updateHoldingValuation(SC_1, asset1, mockValuation); @@ -534,13 +538,13 @@ contract NAVManagerOnTransferTest is NAVManagerTest { } function testOnTransferBasicAuth() public { - vm.expectRevert(); + vm.expectRevert(INAVManager.NotAuthorized.selector); vm.prank(unauthorized); navManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); } function testOnTransferInvalidPoolId() public { - vm.expectRevert(); + vm.expectRevert(INAVManager.InvalidPoolId.selector); vm.prank(hub); navManager.onTransfer(POOL_B, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); } diff --git a/test/managers/unit/SimplePriceManager.t.sol b/test/managers/unit/SimplePriceManager.t.sol new file mode 100644 index 000000000..2f7438228 --- /dev/null +++ b/test/managers/unit/SimplePriceManager.t.sol @@ -0,0 +1,589 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Multicall} from "../../../src/misc/Multicall.sol"; +import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; +import {D18, d18} from "../../../src/misc/types/D18.sol"; +import {IMulticall} from "../../../src/misc/interfaces/IMulticall.sol"; + +import {PoolId} from "../../../src/common/types/PoolId.sol"; +import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; +import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; +import {MAX_MESSAGE_COST} from "../../../src/common/interfaces/IGasService.sol"; + +import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; +import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; +import {ISimplePriceManagerFactory} from "../../../src/managers/interfaces/ISimplePriceManagerFactory.sol"; +import {INAVHook} from "../../../src/managers/interfaces/INavManager.sol"; + +import {IHub} from "../../../src/hub/interfaces/IHub.sol"; +import {IShareClassManager} from "../../../src/hub/interfaces/IShareClassManager.sol"; +import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; + +import "forge-std/Test.sol"; + +contract IsContract {} + +contract MockHub is Multicall { + function notifySharePrice(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external payable {} +} + +contract SimplePriceManagerTest is Test { + PoolId constant POOL_A = PoolId.wrap(1); + PoolId constant POOL_B = PoolId.wrap(2); + ShareClassId constant SC_1 = ShareClassId.wrap(bytes16("1")); + ShareClassId constant SC_2 = ShareClassId.wrap(bytes16("2")); + uint16 constant CENTRIFUGE_ID_1 = 1; + uint16 constant CENTRIFUGE_ID_2 = 2; + uint16 constant CENTRIFUGE_ID_3 = 3; + + AssetId asset1 = newAssetId(1, 1); + AssetId asset2 = newAssetId(2, 1); + + address hub = address(new MockHub()); + address hubRegistry = address(new IsContract()); + address shareClassManager = address(new IsContract()); + + address unauthorized = makeAddr("unauthorized"); + address hubManager = makeAddr("hubManager"); + address manager = makeAddr("manager"); + address caller = makeAddr("caller"); + + SimplePriceManager priceManager; + + function setUp() public virtual { + _setupMocks(); + _deployManager(); + } + + function _setupMocks() internal { + vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.approveDeposits.selector), abi.encode(uint128(0), uint128(0))); + vm.mockCall( + hub, abi.encodeWithSelector(IHub.issueShares.selector), abi.encode(uint128(0), uint128(0), uint128(0)) + ); + vm.mockCall(hub, abi.encodeWithSelector(IHub.approveRedeems.selector), abi.encode(uint128(0))); + vm.mockCall( + hub, abi.encodeWithSelector(IHub.revokeShares.selector), abi.encode(uint128(0), uint128(0), uint128(0)) + ); + vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); + vm.mockCall( + hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_A, hubManager), abi.encode(true) + ); + + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_A), + abi.encode(1) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_1), + abi.encode(100) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_2), + abi.encode(200) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowDepositEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowIssueEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowRedeemEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowRevokeEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + } + + function _deployManager() internal { + priceManager = new SimplePriceManager(POOL_A, SC_1, IHub(hub)); + vm.prank(hubManager); + priceManager.updateManager(manager, true); + + vm.prank(hubManager); + priceManager.updateCaller(caller, true); + + vm.deal(address(priceManager), 1 ether); + } +} + +contract SimplePriceManagerConstructorTest is SimplePriceManagerTest { + function testConstructorSuccess() public view { + assertEq(priceManager.poolId().raw(), POOL_A.raw()); + assertEq(priceManager.scId().raw(), SC_1.raw()); + assertEq(address(priceManager.hub()), hub); + assertEq(address(priceManager.shareClassManager()), shareClassManager); + assertEq(priceManager.globalIssuance(), 0); + assertEq(priceManager.globalNetAssetValue(), 0); + } + + function testConstructorInvalidShareClassCount() public { + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_A), + abi.encode(2) + ); + + vm.expectRevert(ISimplePriceManager.InvalidShareClassCount.selector); + vm.prank(hubManager); + new SimplePriceManager(POOL_A, SC_1, IHub(hub)); + } +} + +contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { + function testSetNetworksSuccess() public { + uint16[] memory networks = new uint16[](3); + networks[0] = CENTRIFUGE_ID_1; + networks[1] = CENTRIFUGE_ID_2; + networks[2] = CENTRIFUGE_ID_3; + + vm.prank(hubManager); + priceManager.setNetworks(networks); + + assertEq(priceManager.networks(0), CENTRIFUGE_ID_1); + assertEq(priceManager.networks(1), CENTRIFUGE_ID_2); + assertEq(priceManager.networks(2), CENTRIFUGE_ID_3); + } + + function testSetNetworksUnauthorized() public { + uint16[] memory networks = new uint16[](1); + networks[0] = CENTRIFUGE_ID_1; + + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.setNetworks(networks); + } + + function testSetNetworksEmpty() public { + uint16[] memory networks = new uint16[](0); + + vm.prank(hubManager); + priceManager.setNetworks(networks); + } + + function testUpdateManagerSuccess() public { + address newManager = makeAddr("newManager"); + + vm.expectEmit(true, true, false, false); + emit ISimplePriceManager.UpdateManager(newManager, true); + + vm.prank(hubManager); + priceManager.updateManager(newManager, true); + + assertTrue(priceManager.manager(newManager)); + } + + function testUpdateManagerRemove() public { + address managerAddr = makeAddr("newManager"); + + vm.prank(hubManager); + priceManager.updateManager(managerAddr, true); + assertTrue(priceManager.manager(managerAddr)); + + vm.expectEmit(true, true, false, false); + emit ISimplePriceManager.UpdateManager(managerAddr, false); + + vm.prank(hubManager); + priceManager.updateManager(managerAddr, false); + + assertFalse(priceManager.manager(managerAddr)); + } + + function testUpdateManagerUnauthorized() public { + address managerAddr = makeAddr("newManager"); + + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.updateManager(managerAddr, true); + } + + function testUpdateManagerZeroAddress() public { + vm.expectRevert(ISimplePriceManager.EmptyAddress.selector); + vm.prank(hubManager); + priceManager.updateManager(address(0), true); + } + + function testUpdateCallerSuccess() public { + address newCaller = makeAddr("newCaller"); + + vm.expectEmit(true, true, false, false); + emit ISimplePriceManager.UpdateCaller(newCaller, true); + + vm.prank(hubManager); + priceManager.updateCaller(newCaller, true); + + assertTrue(priceManager.caller(newCaller)); + } + + function testUpdateCallerRemove() public { + address callerAddr = makeAddr("newCaller"); + + vm.prank(hubManager); + priceManager.updateCaller(callerAddr, true); + assertTrue(priceManager.caller(callerAddr)); + + vm.expectEmit(true, true, false, false); + emit ISimplePriceManager.UpdateCaller(callerAddr, false); + + vm.prank(hubManager); + priceManager.updateCaller(callerAddr, false); + + assertFalse(priceManager.caller(callerAddr)); + } + + function testUpdateCallerUnauthorized() public { + address callerAddr = makeAddr("newCaller"); + + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.updateCaller(callerAddr, true); + } + + function testUpdateCallerZeroAddress() public { + vm.expectRevert(ISimplePriceManager.EmptyAddress.selector); + vm.prank(hubManager); + priceManager.updateCaller(address(0), true); + } +} + +contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { + function setUp() public override { + super.setUp(); + + uint16[] memory networks = new uint16[](2); + networks[0] = CENTRIFUGE_ID_1; + networks[1] = CENTRIFUGE_ID_2; + + vm.prank(hubManager); + priceManager.setNetworks(networks); + } + + function testOnUpdateFirstUpdate() public { + uint128 netAssetValue = 1000; + + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHub.updateSharePrice.selector, POOL_A, SC_1, d18(10, 1)) // 1000/100 = 10 + ); + vm.expectCall( + address(hub), abi.encodeWithSelector(IHub.notifySharePrice.selector, POOL_A, SC_1, CENTRIFUGE_ID_1) + ); + vm.expectCall( + address(hub), abi.encodeWithSelector(IHub.notifySharePrice.selector, POOL_A, SC_1, CENTRIFUGE_ID_2) + ); + + vm.expectEmit(true, true, true, true); + emit ISimplePriceManager.Update(netAssetValue, 100, d18(10, 1)); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, netAssetValue); + + assertEq(priceManager.globalIssuance(), 100); + assertEq(priceManager.globalNetAssetValue(), netAssetValue); + + (uint128 storedNAV, uint128 storedIssuance) = priceManager.metrics(CENTRIFUGE_ID_1); + assertEq(storedNAV, netAssetValue); + assertEq(storedIssuance, 100); + } + + function testOnUpdateSecondNetwork() public { + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + + uint128 netAssetValue2 = 1700; + + // (1000+1700)/(100+200) = 9 + vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateSharePrice.selector, POOL_A, SC_1, d18(9, 1))); + + vm.expectEmit(true, true, true, true); + emit ISimplePriceManager.Update(2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, netAssetValue2); + + assertEq(priceManager.globalIssuance(), 300); // 100 + 200 + assertEq(priceManager.globalNetAssetValue(), 2700); // 1000 + 1700 + } + + function testOnUpdateExistingNetwork() public { + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_1), + abi.encode(150) + ); + + uint128 newNetAssetValue = 1200; + + vm.expectEmit(true, true, true, true); + emit ISimplePriceManager.Update(1200, 150, d18(8, 1)); // 1200/150 = 8 + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, newNetAssetValue); + + assertEq(priceManager.globalIssuance(), 150); + assertEq(priceManager.globalNetAssetValue(), 1200); + } + + function testOnUpdateInvalidPoolId() public { + vm.expectRevert(ISimplePriceManager.InvalidPoolId.selector); + vm.prank(caller); + priceManager.onUpdate(POOL_B, SC_1, CENTRIFUGE_ID_1, 1000); + } + + function testOnUpdateInvalidShareClassId() public { + vm.expectRevert(ISimplePriceManager.InvalidShareClassId.selector); + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_2, CENTRIFUGE_ID_1, 1000); + } + + function testOnUpdateUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + } + + function testOnUpdateZeroIssuance() public { + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_1), + abi.encode(0) + ); + + vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateSharePrice.selector, POOL_A, SC_1, d18(1, 1))); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + + assertEq(priceManager.globalIssuance(), 0); + assertEq(priceManager.globalNetAssetValue(), 1000); + } +} + +contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { + function setUp() public override { + super.setUp(); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, 2000); + } + + function testOnTransferSuccess() public { + uint128 sharesTransferred = 50; + + vm.expectEmit(true, true, false, true); + emit ISimplePriceManager.Transfer(CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); + + vm.prank(caller); + priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); + + (uint128 fromNAV, uint128 fromIssuance) = priceManager.metrics(CENTRIFUGE_ID_1); + (uint128 toNAV, uint128 toIssuance) = priceManager.metrics(CENTRIFUGE_ID_2); + + assertEq(fromIssuance, 50); // 100 - 50 + assertEq(toIssuance, 250); // 200 + 50 + + // NAV should remain unchanged + assertEq(fromNAV, 1000); + assertEq(toNAV, 2000); + } + + function testOnTransferInvalidPoolId() public { + vm.expectRevert(ISimplePriceManager.InvalidPoolId.selector); + vm.prank(caller); + priceManager.onTransfer(POOL_B, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); + } + + function testOnTransferInvalidShareClassId() public { + vm.expectRevert(ISimplePriceManager.InvalidShareClassId.selector); + vm.prank(caller); + priceManager.onTransfer(POOL_A, SC_2, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); + } + + function testOnTransferUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); + } + + function testOnTransferZeroShares() public { + vm.prank(caller); + priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 0); + + (, uint128 fromIssuance) = priceManager.metrics(CENTRIFUGE_ID_1); + (, uint128 toIssuance) = priceManager.metrics(CENTRIFUGE_ID_2); + + assertEq(fromIssuance, 100); + assertEq(toIssuance, 200); + } +} + +contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { + function setUp() public override { + super.setUp(); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + } + + function testApproveDepositsAndIssueSharesSuccess() public { + uint128 approvedAssetAmount = 500; + uint128 extraGasLimit = 100000; + D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 + + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHub.approveDeposits.selector, POOL_A, SC_1, asset1, 1, approvedAssetAmount) + ); + vm.expectCall( + address(hub), + abi.encodeWithSelector( + IHub.issueShares.selector, POOL_A, SC_1, asset1, uint32(1), expectedNavPerShare, extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(asset1, approvedAssetAmount, extraGasLimit); + } + + function testApproveDepositsAndIssueSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveDepositsAndIssueShares(asset1, 500, 100000); + } + + function testApproveDepositsAndIssueSharesMismatchedEpochs() public { + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowDepositEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowIssueEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(asset1, 500, 100000); + } + + function testApproveRedeemsAndRevokeSharesSuccess() public { + uint128 approvedShareAmount = 50; + uint128 extraGasLimit = 100000; + + vm.expectCall( + address(hub), + abi.encodeWithSelector(IHub.approveRedeems.selector, POOL_A, SC_1, asset1, uint32(2), approvedShareAmount) + ); + vm.expectCall( + address(hub), + abi.encodeWithSelector( + IHub.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit + ) // 1000/100 = 10 + ); + + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(asset1, approvedShareAmount, extraGasLimit); + } + + function testApproveRedeemsAndRevokeSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveRedeemsAndRevokeShares(asset1, 50, 100000); + } + + function testApproveRedeemsAndRevokeSharesMismatchedEpochs() public { + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowRedeemEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.nowRevokeEpoch.selector, SC_1, asset1), + abi.encode(3) + ); + + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(asset1, 50, 100000); + } +} + +contract SimplePriceManagerFactoryTest is Test { + PoolId constant POOL_A = PoolId.wrap(1); + PoolId constant POOL_B = PoolId.wrap(2); + ShareClassId constant SC_1 = ShareClassId.wrap(bytes16("1")); + + address hub = address(new IsContract()); + address shareClassManager = address(new IsContract()); + address hubRegistry = address(new IsContract()); + + SimplePriceManagerFactory factory; + + function setUp() public { + _setupMocks(); + factory = new SimplePriceManagerFactory(IHub(hub)); + } + + function _setupMocks() internal { + vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_A), + abi.encode(1) + ); + } + + function testFactoryConstructor() public view { + assertEq(address(factory.hub()), hub); + } + + function testNewManagerSuccess() public { + vm.expectEmit(true, true, false, true); + emit ISimplePriceManagerFactory.DeploySimplePriceManager(POOL_A, SC_1, address(0)); + + ISimplePriceManager manager = factory.newManager(POOL_A, SC_1); + + assertTrue(address(manager) != address(0)); + assertEq(SimplePriceManager(payable(address(manager))).poolId().raw(), POOL_A.raw()); + assertEq(SimplePriceManager(payable(address(manager))).scId().raw(), SC_1.raw()); + assertEq(address(SimplePriceManager(payable(address(manager))).hub()), hub); + assertEq(address(SimplePriceManager(payable(address(manager))).shareClassManager()), shareClassManager); + } + + function testNewManagerInvalidShareClassCount() public { + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_B), + abi.encode(2) + ); + vm.expectRevert(ISimplePriceManagerFactory.InvalidShareClassCount.selector); + factory.newManager(POOL_B, SC_1); + } +} From 00a32f2cf90f9b704732042080d9a4598c03abcc Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:32:13 +0200 Subject: [PATCH 42/83] integration test --- test/managers/integration/NAVManager.t.sol | 255 +++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 test/managers/integration/NAVManager.t.sol diff --git a/test/managers/integration/NAVManager.t.sol b/test/managers/integration/NAVManager.t.sol new file mode 100644 index 000000000..87a38fa95 --- /dev/null +++ b/test/managers/integration/NAVManager.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {console2} from "forge-std/console2.sol"; + +import {D18, d18} from "../../../src/misc/types/D18.sol"; +import {ERC20} from "../../../src/misc/ERC20.sol"; + +import {PoolId} from "../../../src/common/types/PoolId.sol"; +import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; +import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; +import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; +import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; + +import {IHub} from "../../../src/hub/interfaces/IHub.sol"; +import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; +import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; + +import {NAVManager, NAVManagerFactory} from "../../../src/managers/NAVManager.sol"; +import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; +import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; +import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; +import {ISnapshotHook} from "../../../src/common/interfaces/ISnapshotHook.sol"; + +import "../../hub/integration/BaseTest.sol"; + +contract NAVManagerIntegrationTest is BaseTest { + INAVManager public navManager; + ISimplePriceManager public priceManager; + + PoolId constant POOL_A = PoolId.wrap(1); + + ShareClassId scId; + + address manager = makeAddr("manager"); + + AssetId asset1 = USDC_C2; + AssetId asset2 = EUR_STABLE_C2; + AssetId asset3 = newAssetId(CHAIN_CP, 1); + AssetId liabilityAsset = newAssetId(CHAIN_CP, 2); + // differing decimals to test conversion + uint8 asset1Decimals = 6; + uint8 asset2Decimals = 12; + uint8 asset3Decimals = 14; + + function setUp() public override { + super.setUp(); + + cv.registerAsset(asset1, asset1Decimals); + cv.registerAsset(asset2, asset2Decimals); + + vm.prank(address(root)); + hubRegistry.registerAsset(asset3, asset3Decimals); + + vm.prank(address(root)); + hubRegistry.registerAsset(liabilityAsset, 18); + + _setupMocks(); + _setupPool(); + } + + function _setupMocks() internal { + vm.mockCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector), abi.encode()); + } + + function _setupPool() internal { + vm.prank(address(root)); + hubRegistry.registerPool(POOL_A, FM, USD_ID); + + vm.startPrank(FM); + scId = hub.addShareClass(POOL_A, "Test Share Class", "TSC", bytes32("1")); + + navManager = navManagerFactory.newManager(POOL_A); + priceManager = simplePriceManagerFactory.newManager(POOL_A, scId); + + hub.setSnapshotHook(POOL_A, ISnapshotHook(address(navManager))); + hub.updateHubManager(POOL_A, address(navManager), true); + hub.updateHubManager(POOL_A, address(priceManager), true); + navManager.updateManager(manager, true); + priceManager.updateManager(manager, true); + + navManager.setNAVHook(INAVHook(address(priceManager))); + priceManager.updateCaller(address(navManager), true); + + uint16[] memory networks = new uint16[](2); + networks[0] = CHAIN_CP; + networks[1] = CHAIN_CV; + priceManager.setNetworks(networks); + + vm.stopPrank(); + + valuation.setPrice(POOL_A, scId, asset1, d18(1, 1)); + valuation.setPrice(POOL_A, scId, asset2, d18(1, 1)); + valuation.setPrice(POOL_A, scId, asset3, d18(1, 1)); + valuation.setPrice(POOL_A, scId, liabilityAsset, d18(1, 1)); + + vm.deal(address(priceManager), 1 ether); + } + + /// forge-config: default.isolate = true + function testSuccess() public { + vm.startPrank(manager); + navManager.initializeNetwork(CHAIN_CP); + navManager.initializeNetwork(CHAIN_CV); + + navManager.initializeHolding(scId, asset1, IValuation(address(valuation))); + navManager.initializeHolding(scId, asset2, IValuation(address(valuation))); + navManager.initializeHolding(scId, asset3, IValuation(address(valuation))); + navManager.initializeLiability(scId, liabilityAsset, IValuation(address(valuation))); + + cv.updateHoldingAmount(POOL_A, scId, asset1, uint128(1000 * 10 ** asset1Decimals), d18(1, 1), true, false, 0); + cv.updateHoldingAmount(POOL_A, scId, asset2, uint128(2300 * 10 ** asset2Decimals), d18(1, 1), true, false, 1); + + vm.expectCall(address(hub), abi.encodeWithSelector(hub.updateSharePrice.selector, POOL_A, scId, d18(1, 1))); + vm.expectCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector, POOL_A, scId, CHAIN_CP)); + vm.expectCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector, POOL_A, scId, CHAIN_CV)); + cv.updateShares(POOL_A, scId, 3300e18, true, true, 2); + + vm.stopPrank(); + + vm.prank(address(root)); + hub.updateHoldingAmount( + CHAIN_CP, POOL_A, scId, asset3, uint128(500 * 10 ** asset3Decimals), d18(1, 1), true, false, 0 + ); + + vm.expectCall(address(hub), abi.encodeWithSelector(hub.updateSharePrice.selector, POOL_A, scId, d18(1, 1))); + vm.expectCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector, POOL_A, scId, CHAIN_CP)); + vm.expectCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector, POOL_A, scId, CHAIN_CV)); + + vm.prank(address(root)); + hub.updateShares(CHAIN_CP, POOL_A, scId, 500e18, true, true, 1); + + uint128 navHub = navManager.netAssetValue(CHAIN_CP); + uint128 navSpoke = navManager.netAssetValue(CHAIN_CV); + (uint128 navHub2, uint128 issuanceHub) = priceManager.metrics(CHAIN_CP); + (uint128 navSpoke2, uint128 issuanceSpoke) = priceManager.metrics(CHAIN_CV); + uint128 globalNAV = priceManager.globalNetAssetValue(); + uint128 globalIssuance = priceManager.globalIssuance(); + + assertEq(navHub, 500e18); + assertEq(navSpoke, 3300e18); + assertEq(navHub2, navHub); + assertEq(navSpoke2, navSpoke); + assertEq(issuanceHub, 500e18); + assertEq(issuanceSpoke, 3300e18); + assertEq(globalNAV, 3800e18); + assertEq(globalIssuance, 3800e18); + + valuation.setPrice(POOL_A, scId, asset1, d18(11, 10)); // 10% increase in value + valuation.setPrice(POOL_A, scId, asset3, d18(1, 2)); // 50% decrease in value + + vm.prank(manager); + navManager.updateHoldingValue(scId, asset1); + + vm.expectCall( + address(hub), + abi.encodeWithSelector(hub.updateSharePrice.selector, POOL_A, scId, d18(3650e18) / d18(3800e18)) + ); + vm.prank(manager); + navManager.updateHoldingValue(scId, asset3); + + navHub = navManager.netAssetValue(CHAIN_CP); + navSpoke = navManager.netAssetValue(CHAIN_CV); + (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); + (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); + globalNAV = priceManager.globalNetAssetValue(); + globalIssuance = priceManager.globalIssuance(); + (bool spokeGainIsPositive, uint128 spokeGain) = + accounting.accountValue(POOL_A, navManager.gainAccount(CHAIN_CV)); + (bool hubLossIsPositive, uint128 hubLoss) = accounting.accountValue(POOL_A, navManager.lossAccount(CHAIN_CP)); + + assertEq(spokeGain, 100e18); + assertTrue(spokeGainIsPositive); + assertEq(hubLoss, 250e18); + assertFalse(hubLossIsPositive); + assertEq(navHub, 250e18); + assertEq(navSpoke, 3400e18); + assertEq(navHub2, navHub); + assertEq(navSpoke2, navSpoke); + assertEq(issuanceHub, 500e18); + assertEq(issuanceSpoke, 3300e18); + assertEq(globalNAV, 3650e18); // (3300 * 1.1) + (500 * 0.5) = 3650 + assertEq(globalIssuance, 3800e18); + + vm.prank(address(root)); + hub.initiateTransferShares(CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0); + + navHub = navManager.netAssetValue(CHAIN_CP); + navSpoke = navManager.netAssetValue(CHAIN_CV); + (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); + (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); + globalNAV = priceManager.globalNetAssetValue(); + globalIssuance = priceManager.globalIssuance(); + + // NAV and global issuance should remain unchanged, only issuance per network changes + assertEq(navHub, 250e18); + assertEq(navSpoke, 3400e18); + assertEq(navHub2, navHub); + assertEq(navSpoke2, navSpoke); + assertEq(issuanceHub, 370e18); + assertEq(issuanceSpoke, 3430e18); + assertEq(globalNAV, 3650e18); + assertEq(globalIssuance, 3800e18); + + // Increase liability, e.g. fee payable + vm.expectCall( + address(hub), + abi.encodeWithSelector(hub.updateSharePrice.selector, POOL_A, scId, d18(3600e18) / d18(3800e18)) + ); + vm.prank(address(root)); + hub.updateHoldingAmount(CHAIN_CP, POOL_A, scId, liabilityAsset, 50e18, d18(1, 1), true, true, 2); + + navHub = navManager.netAssetValue(CHAIN_CP); + navSpoke = navManager.netAssetValue(CHAIN_CV); + (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); + (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); + globalNAV = priceManager.globalNetAssetValue(); + globalIssuance = priceManager.globalIssuance(); + + // Liability reduces the NAV + assertEq(navHub, 200e18); + assertEq(navSpoke, 3400e18); + assertEq(navHub2, navHub); + assertEq(navSpoke2, navSpoke); + assertEq(issuanceHub, 370e18); + assertEq(issuanceSpoke, 3430e18); + assertEq(globalNAV, 3600e18); + assertEq(globalIssuance, 3800e18); + + // Decrease liability by paying with a cash asset + vm.prank(address(root)); + hub.updateHoldingAmount(CHAIN_CP, POOL_A, scId, liabilityAsset, 50e18, d18(1, 1), false, false, 3); + vm.prank(address(root)); + hub.updateHoldingAmount( + CHAIN_CP, POOL_A, scId, asset3, uint128(100 * 10 ** asset3Decimals), d18(1, 2), false, true, 4 + ); + + navHub = navManager.netAssetValue(CHAIN_CP); + navSpoke = navManager.netAssetValue(CHAIN_CV); + (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); + (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); + globalNAV = priceManager.globalNetAssetValue(); + globalIssuance = priceManager.globalIssuance(); + + // NAV should remain unchanged + assertEq(navHub, 200e18); + assertEq(navSpoke, 3400e18); + assertEq(navHub2, navHub); + assertEq(navSpoke2, navSpoke); + assertEq(issuanceHub, 370e18); + assertEq(issuanceSpoke, 3430e18); + assertEq(globalNAV, 3600e18); + assertEq(globalIssuance, 3800e18); + } +} From f8accd5f91e6d5525fb1fe1dccb8623c3ead3eb2 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:46:53 +0200 Subject: [PATCH 43/83] remove scid --- src/managers/SimplePriceManager.sol | 28 ++++--------------- .../interfaces/ISimplePriceManagerFactory.sol | 4 +-- test/managers/integration/NAVManager.t.sol | 4 +-- test/managers/unit/SimplePriceManager.t.sol | 22 +++++++++++---- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index b6874a89f..72a3f477e 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; import {INAVHook} from "./interfaces/INavManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; import {ISimplePriceManagerFactory} from "./interfaces/ISimplePriceManagerFactory.sol"; @@ -36,15 +35,15 @@ contract SimplePriceManager is ISimplePriceManager { mapping(address => bool) public manager; mapping(address => bool) public caller; - constructor(PoolId poolId_, ShareClassId scId_, IHub hub_) { + constructor(PoolId poolId_, IHub hub_) { poolId = poolId_; - scId = scId_; - hub = hub_; hubRegistry = hub_.hubRegistry(); shareClassManager = hub_.shareClassManager(); require(shareClassManager.shareClassCount(poolId_) == 1, InvalidShareClassCount()); + + scId = shareClassManager.previewShareClassId(poolId_, 1); } /// @dev Check if the msg.sender is a manager @@ -103,40 +102,28 @@ contract SimplePriceManager is ISimplePriceManager { { require(poolId == poolId_, InvalidPoolId()); require(scId == scId_, InvalidShareClassId()); - console2.log("SimplePriceManager onUpdate", netAssetValue); NetworkMetrics storage networkMetrics = metrics[centrifugeId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - console2.log("SCM centid Issuance", issuance); - console2.log("Stored centid Issuance", networkMetrics.issuance); - globalIssuance = globalIssuance + issuance - networkMetrics.issuance; globalNetAssetValue = globalNetAssetValue + netAssetValue - networkMetrics.netAssetValue; D18 price = _navPerShare(); - console2.log("price", price.raw()); - networkMetrics.netAssetValue = netAssetValue; networkMetrics.issuance = issuance; uint256 networkCount = networks.length; - console2.log("networkCount", networkCount); bytes[] memory cs = new bytes[](networkCount + 1); cs[0] = abi.encodeWithSelector(hub.updateSharePrice.selector, poolId, scId, price); for (uint256 i; i < networkCount; i++) { - console2.log("Calling notifySharePrice for centid", networks[i]); cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, networks[i]); } - console2.log("Calling multicall"); - IMulticall(address(hub)).multicall{value: MAX_MESSAGE_COST * (cs.length)}(cs); - console2.log("SimplePriceManager onUpdate done"); - emit Update(globalNetAssetValue, globalIssuance, price); } @@ -174,7 +161,6 @@ contract SimplePriceManager is ISimplePriceManager { require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); D18 navPoolPerShare = _navPerShare(); - console2.log("navPoolPerShare", navPoolPerShare.raw()); hub.approveDeposits(poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount); hub.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); } @@ -215,14 +201,12 @@ contract SimplePriceManagerFactory is ISimplePriceManagerFactory { hub = hub_; } - // TODO: remove scId param - function newManager(PoolId poolId, ShareClassId scId) external returns (ISimplePriceManager) { + function newManager(PoolId poolId) external returns (ISimplePriceManager) { require(hub.shareClassManager().shareClassCount(poolId) == 1, InvalidShareClassCount()); - SimplePriceManager manager = - new SimplePriceManager{salt: keccak256(abi.encode(poolId.raw(), scId.raw()))}(poolId, scId, hub); + SimplePriceManager manager = new SimplePriceManager{salt: keccak256(abi.encode(poolId.raw()))}(poolId, hub); - emit DeploySimplePriceManager(poolId, scId, address(manager)); + emit DeploySimplePriceManager(poolId, address(manager)); return ISimplePriceManager(manager); } } diff --git a/src/managers/interfaces/ISimplePriceManagerFactory.sol b/src/managers/interfaces/ISimplePriceManagerFactory.sol index bd36155b7..d4035a935 100644 --- a/src/managers/interfaces/ISimplePriceManagerFactory.sol +++ b/src/managers/interfaces/ISimplePriceManagerFactory.sol @@ -7,9 +7,9 @@ import {PoolId} from "../../common/types/PoolId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; interface ISimplePriceManagerFactory { - event DeploySimplePriceManager(PoolId indexed poolId, ShareClassId indexed scId, address indexed manager); + event DeploySimplePriceManager(PoolId indexed poolId, address indexed manager); error InvalidShareClassCount(); - function newManager(PoolId poolId, ShareClassId scId) external returns (ISimplePriceManager); + function newManager(PoolId poolId) external returns (ISimplePriceManager); } diff --git a/test/managers/integration/NAVManager.t.sol b/test/managers/integration/NAVManager.t.sol index 87a38fa95..0a21bd9f0 100644 --- a/test/managers/integration/NAVManager.t.sol +++ b/test/managers/integration/NAVManager.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; - import {D18, d18} from "../../../src/misc/types/D18.sol"; import {ERC20} from "../../../src/misc/ERC20.sol"; @@ -71,7 +69,7 @@ contract NAVManagerIntegrationTest is BaseTest { scId = hub.addShareClass(POOL_A, "Test Share Class", "TSC", bytes32("1")); navManager = navManagerFactory.newManager(POOL_A); - priceManager = simplePriceManagerFactory.newManager(POOL_A, scId); + priceManager = simplePriceManagerFactory.newManager(POOL_A); hub.setSnapshotHook(POOL_A, ISnapshotHook(address(navManager))); hub.updateHubManager(POOL_A, address(navManager), true); diff --git a/test/managers/unit/SimplePriceManager.t.sol b/test/managers/unit/SimplePriceManager.t.sol index 2f7438228..531e3d967 100644 --- a/test/managers/unit/SimplePriceManager.t.sol +++ b/test/managers/unit/SimplePriceManager.t.sol @@ -79,6 +79,11 @@ contract SimplePriceManagerTest is Test { abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_A), abi.encode(1) ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.previewShareClassId.selector, POOL_A, 1), + abi.encode(SC_1) + ); vm.mockCall( shareClassManager, abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_1), @@ -112,7 +117,7 @@ contract SimplePriceManagerTest is Test { } function _deployManager() internal { - priceManager = new SimplePriceManager(POOL_A, SC_1, IHub(hub)); + priceManager = new SimplePriceManager(POOL_A, IHub(hub)); vm.prank(hubManager); priceManager.updateManager(manager, true); @@ -142,7 +147,7 @@ contract SimplePriceManagerConstructorTest is SimplePriceManagerTest { vm.expectRevert(ISimplePriceManager.InvalidShareClassCount.selector); vm.prank(hubManager); - new SimplePriceManager(POOL_A, SC_1, IHub(hub)); + new SimplePriceManager(POOL_A, IHub(hub)); } } @@ -558,6 +563,11 @@ contract SimplePriceManagerFactoryTest is Test { abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_A), abi.encode(1) ); + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.previewShareClassId.selector, POOL_A, 1), + abi.encode(SC_1) + ); } function testFactoryConstructor() public view { @@ -565,10 +575,10 @@ contract SimplePriceManagerFactoryTest is Test { } function testNewManagerSuccess() public { - vm.expectEmit(true, true, false, true); - emit ISimplePriceManagerFactory.DeploySimplePriceManager(POOL_A, SC_1, address(0)); + vm.expectEmit(true, false, true, true); + emit ISimplePriceManagerFactory.DeploySimplePriceManager(POOL_A, address(0)); - ISimplePriceManager manager = factory.newManager(POOL_A, SC_1); + ISimplePriceManager manager = factory.newManager(POOL_A); assertTrue(address(manager) != address(0)); assertEq(SimplePriceManager(payable(address(manager))).poolId().raw(), POOL_A.raw()); @@ -584,6 +594,6 @@ contract SimplePriceManagerFactoryTest is Test { abi.encode(2) ); vm.expectRevert(ISimplePriceManagerFactory.InvalidShareClassCount.selector); - factory.newManager(POOL_B, SC_1); + factory.newManager(POOL_B); } } From 20153ffdeab5d44ad29d715ea98dc065088bbd9d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 12:47:24 +0200 Subject: [PATCH 44/83] remove logs --- src/common/MessageDispatcher.sol | 2 -- src/common/MessageProcessor.sol | 3 --- src/hub/Hub.sol | 6 ------ src/hub/HubHelpers.sol | 3 --- src/hub/ShareClassManager.sol | 3 --- src/managers/NAVManager.sol | 5 ----- src/vaults/AsyncRequestManager.sol | 5 ----- 7 files changed, 27 deletions(-) diff --git a/src/common/MessageDispatcher.sol b/src/common/MessageDispatcher.sol index fa2bb262a..4fa2e4e68 100644 --- a/src/common/MessageDispatcher.sol +++ b/src/common/MessageDispatcher.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; import {PoolId} from "./types/PoolId.sol"; import {AssetId} from "./types/AssetId.sol"; import {IRoot} from "./interfaces/IRoot.sol"; @@ -518,7 +517,6 @@ contract MessageDispatcher is Auth, IMessageDispatcher { bytes calldata payload, uint128 extraGasLimit ) external auth { - console2.log("MessageDispatcher sendRequestCallback", assetId.centrifugeId() == localCentrifugeId); if (assetId.centrifugeId() == localCentrifugeId) { spoke.requestCallback(poolId, scId, assetId, payload); } else { diff --git a/src/common/MessageProcessor.sol b/src/common/MessageProcessor.sol index 334085802..4883a75dc 100644 --- a/src/common/MessageProcessor.sol +++ b/src/common/MessageProcessor.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; - import {PoolId} from "./types/PoolId.sol"; import {AssetId} from "./types/AssetId.sol"; import {IRoot} from "./interfaces/IRoot.sol"; @@ -141,7 +139,6 @@ contract MessageProcessor is Auth, IMessageProcessor { MessageLib.UpdateContract memory m = MessageLib.deserializeUpdateContract(message); contractUpdater.execute(PoolId.wrap(m.poolId), ShareClassId.wrap(m.scId), m.target.toAddress(), m.payload); } else if (kind == MessageType.RequestCallback) { - console2.log("MessageProcessor handle RequestCallback"); MessageLib.RequestCallback memory m = MessageLib.deserializeRequestCallback(message); spoke.requestCallback(PoolId.wrap(m.poolId), ShareClassId.wrap(m.scId), AssetId.wrap(m.assetId), m.payload); } else if (kind == MessageType.UpdateVault) { diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index fa2557636..6717f6fe8 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; import {IHoldings} from "./interfaces/IHoldings.sol"; import {IHubHelpers} from "./interfaces/IHubHelpers.sol"; import {IHubRegistry} from "./interfaces/IHubRegistry.sol"; @@ -223,8 +222,6 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar (, D18 poolPerShare) = shareClassManager.metrics(scId); - console2.log("Hub notifySharePrice", centrifugeId); - emit NotifySharePrice(centrifugeId, poolId, scId, poolPerShare); sender.sendNotifyPricePoolPerShare(centrifugeId, poolId, scId, poolPerShare); } @@ -518,7 +515,6 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar /// @inheritdoc IHub function updateSharePrice(PoolId poolId, ShareClassId scId, D18 navPoolPerShare) public payable { _isManager(poolId); - console2.log("SCM updateSharePrice from Hub", navPoolPerShare.raw()); shareClassManager.updateSharePrice(poolId, scId, navPoolPerShare); } @@ -688,8 +684,6 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar hubHelpers.updateAccountingAmount(poolId, scId, assetId, isIncrease, value); } - console2.log("Updated holding", amount); - holdings.setSnapshot(poolId, scId, centrifugeId, isSnapshot, nonce); } diff --git a/src/hub/HubHelpers.sol b/src/hub/HubHelpers.sol index 5ae9eff15..265bff0c1 100644 --- a/src/hub/HubHelpers.sol +++ b/src/hub/HubHelpers.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; - import {IHub, AccountType} from "./interfaces/IHub.sol"; import {IAccounting} from "./interfaces/IAccounting.sol"; import {IHubHelpers} from "./interfaces/IHubHelpers.sol"; @@ -188,7 +186,6 @@ contract HubHelpers is Auth, IHubHelpers { RequestMessageLib.CancelDepositRequest memory m = payload.deserializeCancelDepositRequest(); uint128 cancelledAssetAmount = shareClassManager.cancelDepositRequest(poolId, scId, m.investor, assetId); - console2.log("cancelledAssetAmount", cancelledAssetAmount); // Cancellation might have been queued such that it will be executed in the future during claiming if (cancelledAssetAmount > 0) { sender.sendRequestCallback( diff --git a/src/hub/ShareClassManager.sol b/src/hub/ShareClassManager.sol index 6d178fed3..535de2345 100644 --- a/src/hub/ShareClassManager.sol +++ b/src/hub/ShareClassManager.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; import {IHubRegistry} from "./interfaces/IHubRegistry.sol"; import { IShareClassManager, @@ -337,8 +336,6 @@ contract ShareClassManager is Auth, IShareClassManager { function updateSharePrice(PoolId poolId, ShareClassId scId_, D18 navPoolPerShare) external auth { require(exists(poolId, scId_), ShareClassNotFound()); - console2.log("SCM updateSharePrice", navPoolPerShare.raw()); - ShareClassMetrics storage m = metrics[scId_]; m.navPerShare = navPoolPerShare; emit UpdateShareClass(poolId, scId_, navPoolPerShare); diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index b8f4d7973..d5d71bec6 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {console2} from "forge-std/console2.sol"; - import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; @@ -149,7 +147,6 @@ contract NAVManager is INAVManager { /// @inheritdoc ISnapshotHook function onSync(PoolId poolId_, ShareClassId scId, uint16 centrifugeId) external { - console2.log("NAVManager onSync"); require(msg.sender == address(holdings), NotAuthorized()); require(poolId == poolId_, InvalidPoolId()); _onSync(scId, centrifugeId); @@ -256,9 +253,7 @@ contract NAVManager is INAVManager { require(address(navHook) != address(0), InvalidNAVHook()); uint128 netAssetValue_ = netAssetValue(centrifugeId); - console2.log("NAV", netAssetValue_); navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); - console2.log("NAVManager onSync done"); emit Sync(scId, centrifugeId, netAssetValue_); } diff --git a/src/vaults/AsyncRequestManager.sol b/src/vaults/AsyncRequestManager.sol index 6475d9e41..89e18d9ba 100644 --- a/src/vaults/AsyncRequestManager.sol +++ b/src/vaults/AsyncRequestManager.sol @@ -10,8 +10,6 @@ import {IBaseRequestManager} from "./interfaces/IBaseRequestManager.sol"; import {IAsyncVault, IAsyncRedeemVault} from "./interfaces/IAsyncVault.sol"; import {IAsyncRequestManager, AsyncInvestmentState} from "./interfaces/IVaultManagers.sol"; -import {console2} from "forge-std/console2.sol"; - import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; import {Recoverable} from "../misc/Recoverable.sol"; @@ -174,7 +172,6 @@ contract AsyncRequestManager is Auth, Recoverable, IAsyncRequestManager { revokedShares(poolId, scId, assetId, m.assetAmount, m.shareAmount, D18.wrap(m.pricePoolPerShare)); } else if (kind == uint8(RequestCallbackType.FulfilledDepositRequest)) { RequestCallbackMessageLib.FulfilledDepositRequest memory m = payload.deserializeFulfilledDepositRequest(); - console2.log("RequestCallbackMessageLib.FulfilledDepositRequest", m.cancelledAssetAmount); fulfillDepositRequest( poolId, scId, @@ -258,8 +255,6 @@ contract AsyncRequestManager is Auth, Recoverable, IAsyncRequestManager { IAsyncVault vault_ = IAsyncVault(address(spoke.vault(poolId, scId, assetId, this))); AsyncInvestmentState storage state = investments[vault_][user]; - console2.log("fulfillDepositRequest cancelled", cancelledAssets); - require(state.pendingDepositRequest != 0, NoPendingRequest()); if (cancelledAssets > 0) { require(state.pendingCancelDepositRequest == true, NoPendingRequest()); From 32d5dda436f9b90eed16a14a0059c1115d16a5ef Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:47:36 +0200 Subject: [PATCH 45/83] typo --- src/hub/Hub.sol | 1 + src/managers/SimplePriceManager.sol | 2 +- src/managers/interfaces/ISimplePriceManager.sol | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 598486e87..0f814dd5c 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -516,6 +516,7 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar /// @inheritdoc IHub function updateSharePrice(PoolId poolId, ShareClassId scId, D18 navPoolPerShare) public payable { _isManager(poolId); + shareClassManager.updateSharePrice(poolId, scId, navPoolPerShare); } diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 72a3f477e..8b60f4eab 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {INAVHook} from "./interfaces/INavManager.sol"; +import {INAVHook} from "./interfaces/INAVManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; import {ISimplePriceManagerFactory} from "./interfaces/ISimplePriceManagerFactory.sol"; diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol index 2f21171d5..9649960a7 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -5,7 +5,7 @@ import {D18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {INAVHook} from "./INavManager.sol"; +import {INAVHook} from "./INAVManager.sol"; interface ISimplePriceManager is INAVHook { event Update(uint128 newNAV, uint128 newIssuance, D18 newSharePrice); From da397c5399e1fc475ca9df2a098edc8e054f536e Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:49:31 +0200 Subject: [PATCH 46/83] format --- src/managers/NAVManager.sol | 9 +++++---- src/managers/interfaces/INAVManager.sol | 5 +++-- .../interfaces/ISimplePriceManager.sol | 4 +++- test/integration/ThreeChainEndToEnd.t.sol | 2 +- test/managers/integration/NAVManager.t.sol | 14 +++++++------- test/managers/unit/NAVManager.t.sol | 18 +++++++++--------- test/managers/unit/SimplePriceManager.t.sol | 16 ++++++++-------- 7 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index d5d71bec6..a93a3b4cc 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -1,22 +1,23 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {INAVManager, INAVHook} from "./interfaces/INAVManager.sol"; +import {INAVManagerFactory} from "./interfaces/INAVManagerFactory.sol"; + import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; -import {INAVManagerFactory} from "./interfaces/INAVManagerFactory.sol"; -import {INAVManager, INAVHook} from "./interfaces/INAVManager.sol"; import {PoolId} from "../common/types/PoolId.sol"; import {AssetId} from "../common/types/AssetId.sol"; import {ShareClassId} from "../common/types/ShareClassId.sol"; import {IValuation} from "../common/interfaces/IValuation.sol"; import {ISnapshotHook} from "../common/interfaces/ISnapshotHook.sol"; -import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; -import {IHoldings} from "../hub/interfaces/IHoldings.sol"; import {AccountId, withCentrifugeId} from "../common/types/AccountId.sol"; import {IHub} from "../hub/interfaces/IHub.sol"; +import {IHoldings} from "../hub/interfaces/IHoldings.sol"; import {IAccounting} from "../hub/interfaces/IAccounting.sol"; +import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is INAVManager { diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index 3b4877a87..f14d1a7ca 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; -import {ISnapshotHook} from "../../common/interfaces/ISnapshotHook.sol"; import {D18} from "../../misc/types/D18.sol"; + import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {AccountId} from "../../common/types/AccountId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {IValuation} from "../../common/interfaces/IValuation.sol"; +import {ISnapshotHook} from "../../common/interfaces/ISnapshotHook.sol"; interface INAVHook { /// @notice Callback when there is a new net asset value (NAV) on a specific network. diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol index 9649960a7..a8b24fea9 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {INAVHook} from "./INAVManager.sol"; + import {D18} from "../../misc/types/D18.sol"; + import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {INAVHook} from "./INAVManager.sol"; interface ISimplePriceManager is INAVHook { event Update(uint128 newNAV, uint128 newIssuance, D18 newSharePrice); diff --git a/test/integration/ThreeChainEndToEnd.t.sol b/test/integration/ThreeChainEndToEnd.t.sol index d15990678..10e1a967a 100644 --- a/test/integration/ThreeChainEndToEnd.t.sol +++ b/test/integration/ThreeChainEndToEnd.t.sol @@ -5,8 +5,8 @@ import {EndToEndFlows} from "./EndToEnd.t.sol"; import {LocalAdapter} from "./adapters/LocalAdapter.sol"; import {IntegrationConstants} from "./utils/IntegrationConstants.sol"; -import {CastLib} from "../../src/misc/libraries/CastLib.sol"; import {d18} from "../../src/misc/types/D18.sol"; +import {CastLib} from "../../src/misc/libraries/CastLib.sol"; import {PoolId} from "../../src/common/types/PoolId.sol"; import {ISafe} from "../../src/common/interfaces/IGuardian.sol"; diff --git a/test/managers/integration/NAVManager.t.sol b/test/managers/integration/NAVManager.t.sol index 0a21bd9f0..6def0af5b 100644 --- a/test/managers/integration/NAVManager.t.sol +++ b/test/managers/integration/NAVManager.t.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {D18, d18} from "../../../src/misc/types/D18.sol"; import {ERC20} from "../../../src/misc/ERC20.sol"; +import {D18, d18} from "../../../src/misc/types/D18.sol"; import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; -import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; +import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; +import {ISnapshotHook} from "../../../src/common/interfaces/ISnapshotHook.sol"; +import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; + +import "../../hub/integration/BaseTest.sol"; import {IHub} from "../../../src/hub/interfaces/IHub.sol"; import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; @@ -16,11 +19,8 @@ import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; import {NAVManager, NAVManagerFactory} from "../../../src/managers/NAVManager.sol"; import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; -import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; -import {ISnapshotHook} from "../../../src/common/interfaces/ISnapshotHook.sol"; - -import "../../hub/integration/BaseTest.sol"; +import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; contract NAVManagerIntegrationTest is BaseTest { INAVManager public navManager; diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/unit/NAVManager.t.sol index e999043b9..cf116c655 100644 --- a/test/managers/unit/NAVManager.t.sol +++ b/test/managers/unit/NAVManager.t.sol @@ -1,26 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; import {D18, d18} from "../../../src/misc/types/D18.sol"; +import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; + +import {Mock} from "../../common/mocks/Mock.sol"; +import {MockValuation} from "../../common/mocks/MockValuation.sol"; import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; -import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; - -import {NAVManager, NAVManagerFactory} from "../../../src/managers/NAVManager.sol"; -import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; -import {INAVManagerFactory} from "../../../src/managers/interfaces/INAVManagerFactory.sol"; +import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; +import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; import {IHub} from "../../../src/hub/interfaces/IHub.sol"; import {IHoldings} from "../../../src/hub/interfaces/IHoldings.sol"; import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; -import {MockValuation} from "../../common/mocks/MockValuation.sol"; -import {Mock} from "../../common/mocks/Mock.sol"; +import {NAVManager, NAVManagerFactory} from "../../../src/managers/NAVManager.sol"; +import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; +import {INAVManagerFactory} from "../../../src/managers/interfaces/INAVManagerFactory.sol"; import "forge-std/Test.sol"; diff --git a/test/managers/unit/SimplePriceManager.t.sol b/test/managers/unit/SimplePriceManager.t.sol index 531e3d967..6b5005b62 100644 --- a/test/managers/unit/SimplePriceManager.t.sol +++ b/test/managers/unit/SimplePriceManager.t.sol @@ -1,24 +1,24 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; +import {D18, d18} from "../../../src/misc/types/D18.sol"; import {Multicall} from "../../../src/misc/Multicall.sol"; import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; -import {D18, d18} from "../../../src/misc/types/D18.sol"; import {IMulticall} from "../../../src/misc/interfaces/IMulticall.sol"; import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; +import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; import {MAX_MESSAGE_COST} from "../../../src/common/interfaces/IGasService.sol"; -import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; -import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; -import {ISimplePriceManagerFactory} from "../../../src/managers/interfaces/ISimplePriceManagerFactory.sol"; -import {INAVHook} from "../../../src/managers/interfaces/INavManager.sol"; - import {IHub} from "../../../src/hub/interfaces/IHub.sol"; -import {IShareClassManager} from "../../../src/hub/interfaces/IShareClassManager.sol"; import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; +import {IShareClassManager} from "../../../src/hub/interfaces/IShareClassManager.sol"; + +import {INAVHook} from "../../../src/managers/interfaces/INavManager.sol"; +import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; +import {ISimplePriceManagerFactory} from "../../../src/managers/interfaces/ISimplePriceManagerFactory.sol"; +import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; import "forge-std/Test.sol"; From 550fe0a0ffe84c1188dece521bb801d111f0effe Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:53:08 +0200 Subject: [PATCH 47/83] unused imports --- src/managers/NAVManager.sol | 3 --- src/managers/SimplePriceManager.sol | 1 - src/managers/interfaces/INAVManager.sol | 2 -- src/managers/interfaces/ISimplePriceManager.sol | 2 -- src/managers/interfaces/ISimplePriceManagerFactory.sol | 1 - test/managers/integration/NAVManager.t.sol | 10 +--------- test/managers/unit/NAVManager.t.sol | 3 +-- test/managers/unit/SimplePriceManager.t.sol | 3 --- 8 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index a93a3b4cc..6773c0ecf 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -4,9 +4,6 @@ pragma solidity 0.8.28; import {INAVManager, INAVHook} from "./interfaces/INAVManager.sol"; import {INAVManagerFactory} from "./interfaces/INAVManagerFactory.sol"; -import {Auth} from "../misc/Auth.sol"; -import {D18, d18} from "../misc/types/D18.sol"; - import {PoolId} from "../common/types/PoolId.sol"; import {AssetId} from "../common/types/AssetId.sol"; import {ShareClassId} from "../common/types/ShareClassId.sol"; diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 8b60f4eab..95abdb653 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -5,7 +5,6 @@ import {INAVHook} from "./interfaces/INAVManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; import {ISimplePriceManagerFactory} from "./interfaces/ISimplePriceManagerFactory.sol"; -import {Auth} from "../misc/Auth.sol"; import {D18, d18} from "../misc/types/D18.sol"; import {IMulticall} from "../misc/interfaces/IMulticall.sol"; diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index f14d1a7ca..07b8eddf6 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; -import {D18} from "../../misc/types/D18.sol"; - import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; import {AccountId} from "../../common/types/AccountId.sol"; diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol index a8b24fea9..9302ec729 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -5,9 +5,7 @@ import {INAVHook} from "./INAVManager.sol"; import {D18} from "../../misc/types/D18.sol"; -import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; interface ISimplePriceManager is INAVHook { event Update(uint128 newNAV, uint128 newIssuance, D18 newSharePrice); diff --git a/src/managers/interfaces/ISimplePriceManagerFactory.sol b/src/managers/interfaces/ISimplePriceManagerFactory.sol index d4035a935..3bd979ef7 100644 --- a/src/managers/interfaces/ISimplePriceManagerFactory.sol +++ b/src/managers/interfaces/ISimplePriceManagerFactory.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.28; import {ISimplePriceManager} from "./ISimplePriceManager.sol"; import {PoolId} from "../../common/types/PoolId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; interface ISimplePriceManagerFactory { event DeploySimplePriceManager(PoolId indexed poolId, address indexed manager); diff --git a/test/managers/integration/NAVManager.t.sol b/test/managers/integration/NAVManager.t.sol index 6def0af5b..6873d9bc5 100644 --- a/test/managers/integration/NAVManager.t.sol +++ b/test/managers/integration/NAVManager.t.sol @@ -1,26 +1,18 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {ERC20} from "../../../src/misc/ERC20.sol"; -import {D18, d18} from "../../../src/misc/types/D18.sol"; +import {d18} from "../../../src/misc/types/D18.sol"; import {PoolId} from "../../../src/common/types/PoolId.sol"; import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; import {ISnapshotHook} from "../../../src/common/interfaces/ISnapshotHook.sol"; -import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; import "../../hub/integration/BaseTest.sol"; -import {IHub} from "../../../src/hub/interfaces/IHub.sol"; -import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; -import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; - -import {NAVManager, NAVManagerFactory} from "../../../src/managers/NAVManager.sol"; import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; -import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; contract NAVManagerIntegrationTest is BaseTest { INAVManager public navManager; diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/unit/NAVManager.t.sol index cf116c655..9d48d823c 100644 --- a/test/managers/unit/NAVManager.t.sol +++ b/test/managers/unit/NAVManager.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {D18, d18} from "../../../src/misc/types/D18.sol"; +import {d18} from "../../../src/misc/types/D18.sol"; import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; import {Mock} from "../../common/mocks/Mock.sol"; @@ -9,7 +9,6 @@ import {MockValuation} from "../../common/mocks/MockValuation.sol"; import {PoolId} from "../../../src/common/types/PoolId.sol"; import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; -import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; diff --git a/test/managers/unit/SimplePriceManager.t.sol b/test/managers/unit/SimplePriceManager.t.sol index 6b5005b62..5734d9ab0 100644 --- a/test/managers/unit/SimplePriceManager.t.sol +++ b/test/managers/unit/SimplePriceManager.t.sol @@ -4,18 +4,15 @@ pragma solidity 0.8.28; import {D18, d18} from "../../../src/misc/types/D18.sol"; import {Multicall} from "../../../src/misc/Multicall.sol"; import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; -import {IMulticall} from "../../../src/misc/interfaces/IMulticall.sol"; import {PoolId} from "../../../src/common/types/PoolId.sol"; import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; -import {MAX_MESSAGE_COST} from "../../../src/common/interfaces/IGasService.sol"; import {IHub} from "../../../src/hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; import {IShareClassManager} from "../../../src/hub/interfaces/IShareClassManager.sol"; -import {INAVHook} from "../../../src/managers/interfaces/INavManager.sol"; import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; import {ISimplePriceManagerFactory} from "../../../src/managers/interfaces/ISimplePriceManagerFactory.sol"; import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; From a860c8f9246384a83820427d334c4e026bc70243 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:52:35 +0200 Subject: [PATCH 48/83] close gain/loss --- src/managers/NAVManager.sol | 30 ++++- src/managers/interfaces/INAVManager.sol | 4 + test/managers/unit/NAVManager.t.sol | 144 ++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 6773c0ecf..7bacfd5be 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -190,7 +190,34 @@ contract NAVManager is INAVManager { hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); } - // TODO: realize gain/loss to move to equity account + /// @inheritdoc INAVManager + function closeGainLoss(uint16 centrifugeId) external onlyManager { + require(accountCounter[centrifugeId] > 0, NotInitialized()); + + AccountId equityAccount_ = equityAccount(centrifugeId); + AccountId gainAccount_ = gainAccount(centrifugeId); + AccountId lossAccount_ = lossAccount(centrifugeId); + + (bool gainIsPositive, uint128 gainValue) = accounting.accountValue(poolId, gainAccount_); + (bool lossIsPositive, uint128 lossValue) = accounting.accountValue(poolId, lossAccount_); + + accounting.unlock(poolId); + + // Because we're crediting the gain account for gains and debiting the loss account for losses, + // Gain should never be negative, and loss should never be positive. + // Still, double-check here. + if (gainIsPositive && gainValue > 0) { + accounting.addDebit(gainAccount_, gainValue); + accounting.addCredit(equityAccount_, gainValue); + } + + if (!lossIsPositive && lossValue > 0) { + accounting.addCredit(lossAccount_, lossValue); + accounting.addDebit(equityAccount_, lossValue); + } + + accounting.lock(); + } //---------------------------------------------------------------------------------------------- // Calculations @@ -199,6 +226,7 @@ contract NAVManager is INAVManager { /// @inheritdoc INAVManager function netAssetValue(uint16 centrifugeId) public view returns (uint128) { // TODO: how to handle when one of the accounts is not positive (or positive for loss account) + // Which should never happen, but still... (bool equityIsPositive, uint128 equity) = accounting.accountValue(poolId, equityAccount(centrifugeId)); (bool gainIsPositive, uint128 gain) = accounting.accountValue(poolId, gainAccount(centrifugeId)); (bool lossIsPositive, uint128 loss) = accounting.accountValue(poolId, lossAccount(centrifugeId)); diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index 07b8eddf6..2a24c6dbf 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -115,6 +115,10 @@ interface INAVManager is ISnapshotHook { /// @param accountId The account ID to set function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) external; + /// @notice close gain/loss accounts by moving balances to equity account + /// @param centrifugeId The Centrifuge ID of the network + function closeGainLoss(uint16 centrifugeId) external; + //---------------------------------------------------------------------------------------------- // Calculations //---------------------------------------------------------------------------------------------- diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/unit/NAVManager.t.sol index 9d48d823c..75a3eca9a 100644 --- a/test/managers/unit/NAVManager.t.sol +++ b/test/managers/unit/NAVManager.t.sol @@ -71,6 +71,10 @@ contract NAVManagerTest is Test { vm.mockCall(holdings, abi.encodeWithSelector(IHoldings.snapshot.selector), abi.encode(false, uint64(0))); vm.mockCall(accounting, abi.encodeWithSelector(IAccounting.accountValue.selector), abi.encode(true, uint128(0))); + vm.mockCall(accounting, abi.encodeWithSelector(IAccounting.unlock.selector), abi.encode()); + vm.mockCall(accounting, abi.encodeWithSelector(IAccounting.addDebit.selector), abi.encode()); + vm.mockCall(accounting, abi.encodeWithSelector(IAccounting.addCredit.selector), abi.encode()); + vm.mockCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector), abi.encode()); vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint128)", asset1), abi.encode(6)); vm.mockCall(hubRegistry, abi.encodeWithSignature("decimals(uint128)", asset2), abi.encode(6)); @@ -476,6 +480,146 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { } } +contract NAVManagerCloseGainLossTest is NAVManagerTest { + function setUp() public override { + super.setUp(); + vm.prank(manager); + navManager.initializeNetwork(CENTRIFUGE_ID_1); + } + + function testCloseGainLossSuccess() public { + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 100, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 50, false); + + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.unlock.selector, POOL_A)); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addDebit.selector, navManager.gainAccount(CENTRIFUGE_ID_1), 100) + ); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addCredit.selector, navManager.equityAccount(CENTRIFUGE_ID_1), 100) + ); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addCredit.selector, navManager.lossAccount(CENTRIFUGE_ID_1), 50) + ); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addDebit.selector, navManager.equityAccount(CENTRIFUGE_ID_1), 50) + ); + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); + + vm.prank(manager); + navManager.closeGainLoss(CENTRIFUGE_ID_1); + } + + function testCloseGainLossOnlyGain() public { + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 200, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 0, true); + + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.unlock.selector, POOL_A)); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addDebit.selector, navManager.gainAccount(CENTRIFUGE_ID_1), 200) + ); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addCredit.selector, navManager.equityAccount(CENTRIFUGE_ID_1), 200) + ); + // No calls for loss account + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addCredit.selector, navManager.lossAccount(CENTRIFUGE_ID_1), 0), + 0 + ); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addDebit.selector, navManager.equityAccount(CENTRIFUGE_ID_1), 0), + 0 + ); + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); + + vm.prank(manager); + navManager.closeGainLoss(CENTRIFUGE_ID_1); + } + + function testCloseGainLossOnlyLoss() public { + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 0, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 150, false); + + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.unlock.selector, POOL_A)); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addCredit.selector, navManager.lossAccount(CENTRIFUGE_ID_1), 150) + ); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addDebit.selector, navManager.equityAccount(CENTRIFUGE_ID_1), 150) + ); + // No calls for gain account + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addDebit.selector, navManager.gainAccount(CENTRIFUGE_ID_1), 0), + 0 + ); + vm.expectCall( + accounting, + abi.encodeWithSelector(IAccounting.addCredit.selector, navManager.equityAccount(CENTRIFUGE_ID_1), 0), + 0 + ); + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); + + vm.prank(manager); + navManager.closeGainLoss(CENTRIFUGE_ID_1); + } + + function testCloseGainLossNoGainNoLoss() public { + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 0, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 0, true); + + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.unlock.selector, POOL_A)); + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); + + vm.prank(manager); + navManager.closeGainLoss(CENTRIFUGE_ID_1); + } + + function testCloseGainLossGainNotPositive() public { + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 100, false); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 0, true); + + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.unlock.selector, POOL_A)); + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); + + vm.prank(manager); + navManager.closeGainLoss(CENTRIFUGE_ID_1); + } + + function testCloseGainLossLossIsPositive() public { + _mockAccountValue(navManager.gainAccount(CENTRIFUGE_ID_1), 0, true); + _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 50, true); + + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.unlock.selector, POOL_A)); + vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); + + vm.prank(manager); + navManager.closeGainLoss(CENTRIFUGE_ID_1); + } + + function testCloseGainLossNotInitialized() public { + vm.expectRevert(INAVManager.NotInitialized.selector); + vm.prank(manager); + navManager.closeGainLoss(CENTRIFUGE_ID_2); + } + + function testCloseGainLossUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + navManager.closeGainLoss(CENTRIFUGE_ID_1); + } +} + contract NAVManagerHelperFunctionsTest is NAVManagerTest { function testEquityAccount() public view { AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 1); From ba97ae834e88f6679d207a6b6b42ed88379f8c60 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:54:58 +0200 Subject: [PATCH 49/83] comment --- src/managers/NAVManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 7bacfd5be..f7e97f6fe 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -203,8 +203,8 @@ contract NAVManager is INAVManager { accounting.unlock(poolId); - // Because we're crediting the gain account for gains and debiting the loss account for losses, - // Gain should never be negative, and loss should never be positive. + // Because we're crediting the gain account for gains and debiting the loss account for losses (and loss is + // credit-normal), gain should never be negative, and loss should never be positive. // Still, double-check here. if (gainIsPositive && gainValue > 0) { accounting.addDebit(gainAccount_, gainValue); From 5254e88d04ee053cb6be00ac2c558916b4761fed Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:15:28 +0200 Subject: [PATCH 50/83] comment --- src/managers/NAVManager.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index f7e97f6fe..e55bcf8cf 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -188,6 +188,8 @@ contract NAVManager is INAVManager { onlyManager { hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); + // TODO: update assetIdToAccountId mapping and update value? + // Do we need to do something with the old account/value? } /// @inheritdoc INAVManager From f4e01acad96bb1a8030be4cdda86660c833d8e6c Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Mon, 15 Sep 2025 13:54:50 +0200 Subject: [PATCH 51/83] Apply suggestions from code review Co-authored-by: Jeroen <1748621+hieronx@users.noreply.github.com> --- src/managers/NAVManager.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index e55bcf8cf..53a2271e4 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -39,13 +39,11 @@ contract NAVManager is INAVManager { accounting = hub.accounting(); } - /// @dev Check if the msg.sender is a manager modifier onlyManager() { require(manager[msg.sender], NotAuthorized()); _; } - /// @dev Check if the msg.sender is a hub manager modifier onlyHubManager() { require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); _; @@ -171,9 +169,7 @@ contract NAVManager is INAVManager { function updateHoldingValue(ShareClassId scId, AssetId assetId) public onlyManager { hub.updateHoldingValue(poolId, scId, assetId); (bool isSnapshot,) = holdings.snapshot(poolId, scId, assetId.centrifugeId()); - if (isSnapshot) { - _onSync(scId, assetId.centrifugeId()); - } + if (isSnapshot) _onSync(scId, assetId.centrifugeId()); } /// @inheritdoc INAVManager From ab10917bc1c348ecf0ce0e37a01db2e89374313d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:45:34 +0200 Subject: [PATCH 52/83] remove factories --- script/HubDeployer.s.sol | 49 ++-- src/hub/Hub.sol | 7 + src/managers/NAVManager.sol | 156 ++++++------ src/managers/SimplePriceManager.sol | 147 ++++------- src/managers/interfaces/INAVManager.sol | 62 +++-- .../interfaces/INAVManagerFactory.sol | 15 -- .../interfaces/ISimplePriceManager.sol | 63 +++-- .../interfaces/ISimplePriceManagerFactory.sol | 14 -- test/managers/integration/NAVManager.t.sol | 98 ++++---- test/managers/unit/NAVManager.t.sol | 218 ++++++---------- test/managers/unit/SimplePriceManager.t.sol | 238 ++++-------------- 11 files changed, 405 insertions(+), 662 deletions(-) delete mode 100644 src/managers/interfaces/INAVManagerFactory.sol delete mode 100644 src/managers/interfaces/ISimplePriceManagerFactory.sol diff --git a/script/HubDeployer.s.sol b/script/HubDeployer.s.sol index 7ab204931..948106245 100644 --- a/script/HubDeployer.s.sol +++ b/script/HubDeployer.s.sol @@ -12,8 +12,8 @@ import {HubHelpers} from "../src/hub/HubHelpers.sol"; import {HubRegistry} from "../src/hub/HubRegistry.sol"; import {ShareClassManager} from "../src/hub/ShareClassManager.sol"; -import {NAVManagerFactory} from "../src/managers/NAVManager.sol"; -import {SimplePriceManagerFactory} from "../src/managers/SimplePriceManager.sol"; +import {NAVManager} from "../src/managers/NAVManager.sol"; +import {SimplePriceManager} from "../src/managers/SimplePriceManager.sol"; import "forge-std/Script.sol"; @@ -31,6 +31,8 @@ struct HubReport { ShareClassManager shareClassManager; HubHelpers hubHelpers; Hub hub; + NAVManager navManager; + SimplePriceManager simplePriceManager; } contract HubActionBatcher is CommonActionBatcher, HubConstants { @@ -45,6 +47,8 @@ contract HubActionBatcher is CommonActionBatcher, HubConstants { report.common.messageDispatcher.rely(address(report.hub)); report.common.multiAdapter.rely(address(report.hub)); report.common.poolEscrowFactory.rely(address(report.hub)); + report.navManager.rely(address(report.hub)); + report.simplePriceManager.rely(address(report.hub)); // Rely hub helpers report.accounting.rely(address(report.hubHelpers)); @@ -56,6 +60,10 @@ contract HubActionBatcher is CommonActionBatcher, HubConstants { report.hub.rely(address(report.common.messageDispatcher)); report.hub.rely(address(report.common.guardian)); + // Rely other + report.simplePriceManager.rely(address(report.navManager)); + report.navManager.rely(address(report.holdings)); + // Rely root report.hubRegistry.rely(address(report.common.root)); report.accounting.rely(address(report.common.root)); @@ -87,6 +95,8 @@ contract HubActionBatcher is CommonActionBatcher, HubConstants { report.shareClassManager.deny(address(this)); report.hub.deny(address(this)); report.hubHelpers.deny(address(this)); + report.navManager.deny(address(this)); + report.simplePriceManager.deny(address(this)); } } @@ -98,8 +108,8 @@ contract HubDeployer is CommonDeployer, HubConstants { ShareClassManager public shareClassManager; HubHelpers public hubHelpers; Hub public hub; - NAVManagerFactory public navManagerFactory; - SimplePriceManagerFactory public simplePriceManagerFactory; + NAVManager public navManager; + SimplePriceManager public simplePriceManager; function deployHub(CommonInput memory input, HubActionBatcher batcher) public { _preDeployHub(input, batcher); @@ -167,19 +177,18 @@ contract HubDeployer is CommonDeployer, HubConstants { ) ); - navManagerFactory = NAVManagerFactory( + navManager = NAVManager( create3( - generateSalt("navManagerFactory"), - abi.encodePacked(type(NAVManagerFactory).creationCode, abi.encode(hub)) + generateSalt("navManager"), + abi.encodePacked(type(NAVManager).creationCode, abi.encode(hub, address(batcher))) ) ); - simplePriceManagerFactory = SimplePriceManagerFactory( - create3( - generateSalt("simplePriceManagerFactory"), - abi.encodePacked(type(SimplePriceManagerFactory).creationCode, abi.encode(hub)) - ) + address simplePriceManagerAddr = create3( + generateSalt("simplePriceManager"), + abi.encodePacked(type(SimplePriceManager).creationCode, abi.encode(hub, address(batcher))) ); + simplePriceManager = SimplePriceManager(payable(simplePriceManagerAddr)); batcher.engageHub(_hubReport()); @@ -189,8 +198,8 @@ contract HubDeployer is CommonDeployer, HubConstants { register("shareClassManager", address(shareClassManager)); register("hubHelpers", address(hubHelpers)); register("hub", address(hub)); - register("navManagerFactory", address(navManagerFactory)); - register("simplePriceManagerFactory", address(simplePriceManagerFactory)); + register("navManager", address(navManager)); + register("simplePriceManager", address(simplePriceManager)); } function _postDeployHub(HubActionBatcher batcher) internal { @@ -203,6 +212,16 @@ contract HubDeployer is CommonDeployer, HubConstants { } function _hubReport() internal view returns (HubReport memory) { - return HubReport(_commonReport(), hubRegistry, accounting, holdings, shareClassManager, hubHelpers, hub); + return HubReport( + _commonReport(), + hubRegistry, + accounting, + holdings, + shareClassManager, + hubHelpers, + hub, + navManager, + simplePriceManager + ); } } diff --git a/src/hub/Hub.sol b/src/hub/Hub.sol index 0f814dd5c..fcc2c1da8 100644 --- a/src/hub/Hub.sol +++ b/src/hub/Hub.sol @@ -589,6 +589,13 @@ contract Hub is Multicall, Auth, Recoverable, IHub, IHubGatewayHandler, IHubGuar (bool isPositive, uint128 diff) = holdings.update(poolId, scId, assetId); hubHelpers.updateAccountingValue(poolId, scId, assetId, isPositive, diff); + + (bool isSnapshot,) = holdings.snapshot(poolId, scId, assetId.centrifugeId()); + + if (isSnapshot) { + ISnapshotHook hook = holdings.snapshotHook(poolId); + if (address(hook) != address(0)) hook.onSync(poolId, scId, assetId.centrifugeId()); + } } /// @inheritdoc IHub diff --git a/src/managers/NAVManager.sol b/src/managers/NAVManager.sol index 53a2271e4..c367bbbce 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/NAVManager.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.28; import {INAVManager, INAVHook} from "./interfaces/INAVManager.sol"; -import {INAVManagerFactory} from "./interfaces/INAVManagerFactory.sol"; + +import {Auth} from "../misc/Auth.sol"; import {PoolId} from "../common/types/PoolId.sol"; import {AssetId} from "../common/types/AssetId.sol"; @@ -17,34 +18,30 @@ import {IAccounting} from "../hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. -contract NAVManager is INAVManager { - PoolId public immutable poolId; - +contract NAVManager is INAVManager, Auth { IHub public immutable hub; IHubRegistry public immutable hubRegistry; IHoldings public immutable holdings; IAccounting public immutable accounting; INAVHook public navHook; - mapping(uint16 centrifugeId => uint16) public accountCounter; - mapping(uint16 centrifugeId => mapping(AssetId => AccountId)) public assetIdToAccountId; - mapping(address => bool) public manager; - - constructor(PoolId poolId_, IHub hub_) { - poolId = poolId_; + mapping(PoolId poolId => mapping(uint16 centrifugeId => uint16)) public accountCounter; + mapping(PoolId poolId => mapping(AssetId => AccountId)) public assetIdToAccountId; + mapping(PoolId poolId => mapping(address => bool)) public manager; + constructor(IHub hub_, address deployer) Auth(deployer) { hub = hub_; hubRegistry = hub_.hubRegistry(); holdings = hub.holdings(); accounting = hub.accounting(); } - modifier onlyManager() { - require(manager[msg.sender], NotAuthorized()); + modifier onlyManager(PoolId poolId) { + require(manager[poolId][msg.sender], NotAuthorized()); _; } - modifier onlyHubManager() { + modifier onlyHubManager(PoolId poolId) { require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); _; } @@ -54,18 +51,16 @@ contract NAVManager is INAVManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVManager - function setNAVHook(INAVHook navHook_) external onlyHubManager { + function setNAVHook(PoolId poolId, INAVHook navHook_) external onlyHubManager(poolId) { navHook = navHook_; - emit SetNavHook(address(navHook_)); + emit SetNavHook(poolId, address(navHook_)); } /// @inheritdoc INAVManager - function updateManager(address manager_, bool canManage) external onlyHubManager { - require(manager_ != address(0), EmptyAddress()); - - manager[manager_] = canManage; + function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { + manager[poolId][manager_] = canManage; - emit UpdateManager(manager_, canManage); + emit UpdateManager(poolId, manager_, canManage); } //---------------------------------------------------------------------------------------------- @@ -73,30 +68,33 @@ contract NAVManager is INAVManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVManager - function initializeNetwork(uint16 centrifugeId) external onlyManager { - require(accountCounter[centrifugeId] == 0, AlreadyInitialized()); + function initializeNetwork(PoolId poolId, uint16 centrifugeId) external onlyManager(poolId) { + require(accountCounter[poolId][centrifugeId] == 0, AlreadyInitialized()); hub.createAccount(poolId, equityAccount(centrifugeId), false); hub.createAccount(poolId, liabilityAccount(centrifugeId), false); hub.createAccount(poolId, gainAccount(centrifugeId), false); hub.createAccount(poolId, lossAccount(centrifugeId), false); - accountCounter[centrifugeId] = 5; + accountCounter[poolId][centrifugeId] = 5; - emit InitializeNetwork(centrifugeId); + emit InitializeNetwork(poolId, centrifugeId); } /// @inheritdoc INAVManager - function initializeHolding(ShareClassId scId, AssetId assetId, IValuation valuation) external onlyManager { + function initializeHolding(PoolId poolId, ShareClassId scId, AssetId assetId, IValuation valuation) + external + onlyManager(poolId) + { uint16 centrifugeId = assetId.centrifugeId(); - uint16 index = accountCounter[centrifugeId]; + uint16 index = accountCounter[poolId][centrifugeId]; require(index > 0, NotInitialized()); require(index < type(uint16).max, ExceedsMaxAccounts()); - AccountId assetAccount_ = assetIdToAccountId[centrifugeId][assetId]; + AccountId assetAccount_ = assetIdToAccountId[poolId][assetId]; if (assetAccount_.isNull()) { assetAccount_ = withCentrifugeId(centrifugeId, index); - assetIdToAccountId[centrifugeId][assetId] = assetAccount_; + assetIdToAccountId[poolId][assetId] = assetAccount_; } hub.createAccount(poolId, assetAccount_, true); @@ -111,77 +109,81 @@ contract NAVManager is INAVManager { lossAccount(centrifugeId) ); - accountCounter[centrifugeId] = index + 1; + accountCounter[poolId][centrifugeId] = index + 1; - emit InitializeHolding(scId, assetId); + emit InitializeHolding(poolId, scId, assetId); } /// @inheritdoc INAVManager - function initializeLiability(ShareClassId scId, AssetId assetId, IValuation valuation) external onlyManager { + function initializeLiability(PoolId poolId, ShareClassId scId, AssetId assetId, IValuation valuation) + external + onlyManager(poolId) + { uint16 centrifugeId = assetId.centrifugeId(); - uint16 index = accountCounter[centrifugeId]; + uint16 index = accountCounter[poolId][centrifugeId]; require(index > 0, NotInitialized()); require(index < type(uint16).max, ExceedsMaxAccounts()); - AccountId expenseAccount_ = assetIdToAccountId[centrifugeId][assetId]; + AccountId expenseAccount_ = assetIdToAccountId[poolId][assetId]; if (expenseAccount_.isNull()) { expenseAccount_ = withCentrifugeId(centrifugeId, index); - assetIdToAccountId[centrifugeId][assetId] = expenseAccount_; + assetIdToAccountId[poolId][assetId] = expenseAccount_; } hub.createAccount(poolId, expenseAccount_, true); hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount_, liabilityAccount(centrifugeId)); - accountCounter[centrifugeId] = index + 1; + accountCounter[poolId][centrifugeId] = index + 1; - emit InitializeLiability(scId, assetId); + emit InitializeLiability(poolId, scId, assetId); } //---------------------------------------------------------------------------------------------- - // Price updates + // INAVHook updates //---------------------------------------------------------------------------------------------- /// @inheritdoc ISnapshotHook - function onSync(PoolId poolId_, ShareClassId scId, uint16 centrifugeId) external { - require(msg.sender == address(holdings), NotAuthorized()); - require(poolId == poolId_, InvalidPoolId()); - _onSync(scId, centrifugeId); + function onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external auth { + _onSync(poolId, scId, centrifugeId); } /// @inheritdoc ISnapshotHook function onTransfer( - PoolId poolId_, - ShareClassId scId_, + PoolId poolId, + ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId, uint128 sharesTransferred - ) external { - require(msg.sender == address(hub), NotAuthorized()); - require(poolId == poolId_, InvalidPoolId()); + ) external auth { require(address(navHook) != address(0), InvalidNAVHook()); - navHook.onTransfer(poolId, scId_, fromCentrifugeId, toCentrifugeId, sharesTransferred); + navHook.onTransfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); - emit Transfer(scId_, fromCentrifugeId, toCentrifugeId, sharesTransferred); + emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); } + //---------------------------------------------------------------------------------------------- + // Holding updates + //---------------------------------------------------------------------------------------------- + /// @inheritdoc INAVManager - function updateHoldingValue(ShareClassId scId, AssetId assetId) public onlyManager { + function updateHoldingValue(PoolId poolId, ShareClassId scId, AssetId assetId) external onlyManager(poolId) { hub.updateHoldingValue(poolId, scId, assetId); - (bool isSnapshot,) = holdings.snapshot(poolId, scId, assetId.centrifugeId()); - if (isSnapshot) _onSync(scId, assetId.centrifugeId()); } /// @inheritdoc INAVManager - function updateHoldingValuation(ShareClassId scId, AssetId assetId, IValuation valuation) external onlyManager { + function updateHoldingValuation(PoolId poolId, ShareClassId scId, AssetId assetId, IValuation valuation) + external + onlyManager(poolId) + { hub.updateHoldingValuation(poolId, scId, assetId, valuation); - updateHoldingValue(scId, assetId); + hub.updateHoldingValue(poolId, scId, assetId); } /// @inheritdoc INAVManager - function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) + function setHoldingAccountId(PoolId poolId, ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) external - onlyManager + onlyManager(poolId) { hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); // TODO: update assetIdToAccountId mapping and update value? @@ -189,8 +191,8 @@ contract NAVManager is INAVManager { } /// @inheritdoc INAVManager - function closeGainLoss(uint16 centrifugeId) external onlyManager { - require(accountCounter[centrifugeId] > 0, NotInitialized()); + function closeGainLoss(PoolId poolId, uint16 centrifugeId) external onlyManager(poolId) { + require(accountCounter[poolId][centrifugeId] > 0, NotInitialized()); AccountId equityAccount_ = equityAccount(centrifugeId); AccountId gainAccount_ = gainAccount(centrifugeId); @@ -222,15 +224,15 @@ contract NAVManager is INAVManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVManager - function netAssetValue(uint16 centrifugeId) public view returns (uint128) { - // TODO: how to handle when one of the accounts is not positive (or positive for loss account) - // Which should never happen, but still... + function netAssetValue(PoolId poolId, uint16 centrifugeId) public view returns (uint128) { (bool equityIsPositive, uint128 equity) = accounting.accountValue(poolId, equityAccount(centrifugeId)); (bool gainIsPositive, uint128 gain) = accounting.accountValue(poolId, gainAccount(centrifugeId)); (bool lossIsPositive, uint128 loss) = accounting.accountValue(poolId, lossAccount(centrifugeId)); (bool liabilityIsPositive, uint128 liability) = accounting.accountValue(poolId, liabilityAccount(centrifugeId)); - require(equityIsPositive && gainIsPositive && liabilityIsPositive && (!lossIsPositive || loss == 0), ""); + require( + equityIsPositive && gainIsPositive && liabilityIsPositive && (!lossIsPositive || loss == 0), InvalidNAV() + ); return equity + gain - loss - liability; } @@ -240,13 +242,13 @@ contract NAVManager is INAVManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVManager - function assetAccount(uint16 centrifugeId, AssetId assetId) public view returns (AccountId) { - return assetIdToAccountId[centrifugeId][assetId]; + function assetAccount(PoolId poolId, AssetId assetId) public view returns (AccountId) { + return assetIdToAccountId[poolId][assetId]; } /// @inheritdoc INAVManager - function expenseAccount(uint16 centrifugeId, AssetId assetId) public view returns (AccountId) { - return assetAccount(centrifugeId, assetId); + function expenseAccount(PoolId poolId, AssetId assetId) public view returns (AccountId) { + return assetAccount(poolId, assetId); } /// @inheritdoc INAVManager @@ -273,30 +275,12 @@ contract NAVManager is INAVManager { // Internal methods //---------------------------------------------------------------------------------------------- - function _onSync(ShareClassId scId, uint16 centrifugeId) internal { + function _onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) internal { require(address(navHook) != address(0), InvalidNAVHook()); - uint128 netAssetValue_ = netAssetValue(centrifugeId); + uint128 netAssetValue_ = netAssetValue(poolId, centrifugeId); navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); - emit Sync(scId, centrifugeId, netAssetValue_); - } -} - -contract NAVManagerFactory is INAVManagerFactory { - IHub public immutable hub; - - constructor(IHub hub_) { - hub = hub_; - } - - /// @inheritdoc INAVManagerFactory - function newManager(PoolId poolId) external returns (INAVManager) { - require(hub.hubRegistry().exists(poolId), InvalidPoolId()); - - NAVManager manager = new NAVManager{salt: bytes32(uint256(poolId.raw()))}(poolId, hub); - - emit DeployNavManager(poolId, address(manager)); - return INAVManager(manager); + emit Sync(poolId, scId, centrifugeId, netAssetValue_); } } diff --git a/src/managers/SimplePriceManager.sol b/src/managers/SimplePriceManager.sol index 95abdb653..5fa4880a7 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/SimplePriceManager.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.28; import {INAVHook} from "./interfaces/INAVManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; -import {ISimplePriceManagerFactory} from "./interfaces/ISimplePriceManagerFactory.sol"; import {D18, d18} from "../misc/types/D18.sol"; import {IMulticall} from "../misc/interfaces/IMulticall.sol"; +import {Auth} from "../misc/Auth.sol"; import {PoolId} from "../common/types/PoolId.sol"; import {AssetId} from "../common/types/AssetId.sol"; @@ -18,76 +18,51 @@ import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; import {IShareClassManager} from "../hub/interfaces/IShareClassManager.sol"; /// @notice Share price calculation manager for single share class pools. -contract SimplePriceManager is ISimplePriceManager { - PoolId public immutable poolId; - ShareClassId public immutable scId; - +contract SimplePriceManager is ISimplePriceManager, Auth { IHub public immutable hub; IHubRegistry public immutable hubRegistry; IShareClassManager public immutable shareClassManager; - uint16[] public networks; - uint128 public globalIssuance; - uint128 public globalNetAssetValue; - mapping(uint16 centrifugeId => NetworkMetrics) public metrics; - - mapping(address => bool) public manager; - mapping(address => bool) public caller; + mapping(PoolId poolId => uint16[]) public networks; + mapping(PoolId poolId => uint128) public globalIssuance; + mapping(PoolId poolId => uint128) public globalNetAssetValue; + mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public metrics; + mapping(PoolId poolId => mapping(address => bool)) public manager; - constructor(PoolId poolId_, IHub hub_) { - poolId = poolId_; + constructor(IHub hub_, address deployer) Auth(deployer) { hub = hub_; hubRegistry = hub_.hubRegistry(); shareClassManager = hub_.shareClassManager(); - require(shareClassManager.shareClassCount(poolId_) == 1, InvalidShareClassCount()); - - scId = shareClassManager.previewShareClassId(poolId_, 1); + // TODO: where to check share class count? + // require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); + // scId = shareClassManager.previewShareClassId(poolId, 1); } - /// @dev Check if the msg.sender is a manager - modifier onlyManager() { - require(manager[msg.sender], NotAuthorized()); + modifier onlyManager(PoolId poolId) { + require(manager[poolId][msg.sender], NotAuthorized()); _; } - /// @dev Check if the msg.sender is a hub manager - modifier onlyHubManager() { + modifier onlyHubManager(PoolId poolId) { require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); _; } - /// @dev Check if the msg.sender is a allowed caller - modifier onlyCaller() { - require(caller[msg.sender], NotAuthorized()); - _; - } - //---------------------------------------------------------------------------------------------- // Administration //---------------------------------------------------------------------------------------------- /// @inheritdoc ISimplePriceManager - function setNetworks(uint16[] calldata centrifugeIds) external onlyHubManager { - networks = centrifugeIds; + function setNetworks(PoolId poolId, uint16[] calldata centrifugeIds) external onlyHubManager(poolId) { + networks[poolId] = centrifugeIds; } /// @inheritdoc ISimplePriceManager - function updateManager(address manager_, bool canManage) external onlyHubManager { - require(manager_ != address(0), EmptyAddress()); - - manager[manager_] = canManage; - - emit UpdateManager(manager_, canManage); - } - - /// @inheritdoc ISimplePriceManager - function updateCaller(address caller_, bool canCall) external onlyHubManager { - require(caller_ != address(0), EmptyAddress()); - - caller[caller_] = canCall; + function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { + manager[poolId][manager_] = canManage; - emit UpdateCaller(caller_, canCall); + emit UpdateManager(poolId, manager_, canManage); } //---------------------------------------------------------------------------------------------- @@ -95,54 +70,45 @@ contract SimplePriceManager is ISimplePriceManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVHook - function onUpdate(PoolId poolId_, ShareClassId scId_, uint16 centrifugeId, uint128 netAssetValue) - external - onlyCaller - { - require(poolId == poolId_, InvalidPoolId()); - require(scId == scId_, InvalidShareClassId()); - - NetworkMetrics storage networkMetrics = metrics[centrifugeId]; + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { + NetworkMetrics storage networkMetrics = metrics[poolId][centrifugeId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - globalIssuance = globalIssuance + issuance - networkMetrics.issuance; - globalNetAssetValue = globalNetAssetValue + netAssetValue - networkMetrics.netAssetValue; + globalIssuance[poolId] = globalIssuance[poolId] + issuance - networkMetrics.issuance; + globalNetAssetValue[poolId] = globalNetAssetValue[poolId] + netAssetValue - networkMetrics.netAssetValue; - D18 price = _navPerShare(); + D18 price = _navPerShare(poolId); networkMetrics.netAssetValue = netAssetValue; networkMetrics.issuance = issuance; - uint256 networkCount = networks.length; + uint256 networkCount = networks[poolId].length; bytes[] memory cs = new bytes[](networkCount + 1); cs[0] = abi.encodeWithSelector(hub.updateSharePrice.selector, poolId, scId, price); for (uint256 i; i < networkCount; i++) { - cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, networks[i]); + cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, networks[poolId][i]); } IMulticall(address(hub)).multicall{value: MAX_MESSAGE_COST * (cs.length)}(cs); - emit Update(globalNetAssetValue, globalIssuance, price); + emit Update(poolId, globalNetAssetValue[poolId], globalIssuance[poolId], price); } /// @inheritdoc INAVHook function onTransfer( - PoolId poolId_, - ShareClassId scId_, + PoolId poolId, + ShareClassId, uint16 fromCentrifugeId, uint16 toCentrifugeId, uint128 sharesTransferred - ) external onlyCaller { - require(poolId == poolId_, InvalidPoolId()); - require(scId == scId_, InvalidShareClassId()); - - NetworkMetrics storage fromMetrics = metrics[fromCentrifugeId]; - NetworkMetrics storage toMetrics = metrics[toCentrifugeId]; + ) external auth { + NetworkMetrics storage fromMetrics = metrics[poolId][fromCentrifugeId]; + NetworkMetrics storage toMetrics = metrics[poolId][toCentrifugeId]; fromMetrics.issuance -= sharesTransferred; toMetrics.issuance += sharesTransferred; - emit Transfer(fromCentrifugeId, toCentrifugeId, sharesTransferred); + emit Transfer(poolId, fromCentrifugeId, toCentrifugeId, sharesTransferred); } //---------------------------------------------------------------------------------------------- @@ -150,31 +116,37 @@ contract SimplePriceManager is ISimplePriceManager { //---------------------------------------------------------------------------------------------- /// @inheritdoc ISimplePriceManager - function approveDepositsAndIssueShares(AssetId depositAssetId, uint128 approvedAssetAmount, uint128 extraGasLimit) - external - onlyManager - { + function approveDepositsAndIssueShares( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external onlyManager(poolId) { uint32 nowDepositEpochId = shareClassManager.nowDepositEpoch(scId, depositAssetId); uint32 nowIssueEpochId = shareClassManager.nowIssueEpoch(scId, depositAssetId); require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); - D18 navPoolPerShare = _navPerShare(); + D18 navPoolPerShare = _navPerShare(poolId); hub.approveDeposits(poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount); hub.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); } /// @inheritdoc ISimplePriceManager - function approveRedeemsAndRevokeShares(AssetId payoutAssetId, uint128 approvedShareAmount, uint128 extraGasLimit) - external - onlyManager - { + function approveRedeemsAndRevokeShares( + PoolId poolId, + ShareClassId scId, + AssetId payoutAssetId, + uint128 approvedShareAmount, + uint128 extraGasLimit + ) external onlyManager(poolId) { uint32 nowRedeemEpochId = shareClassManager.nowRedeemEpoch(scId, payoutAssetId); uint32 nowRevokeEpochId = shareClassManager.nowRevokeEpoch(scId, payoutAssetId); require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); - D18 navPoolPerShare = _navPerShare(); + D18 navPoolPerShare = _navPerShare(poolId); hub.approveRedeems(poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount); hub.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); } @@ -183,8 +155,8 @@ contract SimplePriceManager is ISimplePriceManager { // Helpers //---------------------------------------------------------------------------------------------- - function _navPerShare() internal view returns (D18) { - return globalIssuance == 0 ? d18(1, 1) : d18(globalNetAssetValue) / d18(globalIssuance); + function _navPerShare(PoolId poolId) internal view returns (D18) { + return globalIssuance[poolId] == 0 ? d18(1, 1) : d18(globalNetAssetValue[poolId]) / d18(globalIssuance[poolId]); } // TODO: remove when not needed anymore @@ -192,20 +164,3 @@ contract SimplePriceManager is ISimplePriceManager { // Accept ETH refunds from multicall } } - -contract SimplePriceManagerFactory is ISimplePriceManagerFactory { - IHub public immutable hub; - - constructor(IHub hub_) { - hub = hub_; - } - - function newManager(PoolId poolId) external returns (ISimplePriceManager) { - require(hub.shareClassManager().shareClassCount(poolId) == 1, InvalidShareClassCount()); - - SimplePriceManager manager = new SimplePriceManager{salt: keccak256(abi.encode(poolId.raw()))}(poolId, hub); - - emit DeploySimplePriceManager(poolId, address(manager)); - return ISimplePriceManager(manager); - } -} diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/interfaces/INAVManager.sol index 2a24c6dbf..dcb1916f4 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/interfaces/INAVManager.sol @@ -32,14 +32,15 @@ interface INAVHook { } interface INAVManager is ISnapshotHook { - event UpdateManager(address indexed manager, bool canManage); - event SetNavHook(address indexed navHook); - event InitializeNetwork(uint16 indexed centrifugeId); - event InitializeHolding(ShareClassId indexed scId, AssetId indexed assetId); - event InitializeLiability(ShareClassId indexed scId, AssetId indexed assetId); - event Sync(ShareClassId indexed scId, uint16 indexed centrifugeId, uint128 netAssetValue); + event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); + event SetNavHook(PoolId indexed poolId, address indexed navHook); + event InitializeNetwork(PoolId indexed poolId, uint16 indexed centrifugeId); + event InitializeHolding(PoolId indexed poolId, ShareClassId indexed scId, AssetId indexed assetId); + event InitializeLiability(PoolId indexed poolId, ShareClassId indexed scId, AssetId indexed assetId); + event Sync(PoolId indexed poolId, ShareClassId indexed scId, uint16 indexed centrifugeId, uint128 netAssetValue); event Transfer( - ShareClassId indexed scId_, + PoolId indexed poolId, + ShareClassId scId, uint16 indexed fromCentrifugeId, uint16 indexed toCentrifugeId, uint128 sharesTransferred @@ -50,9 +51,7 @@ interface INAVManager is ISnapshotHook { error NotInitialized(); error ExceedsMaxAccounts(); error InvalidNAVHook(); - error InvalidPoolId(); - error EmptyAddress(); - error NotAuthorized(); + error InvalidNAV(); //---------------------------------------------------------------------------------------------- // Administration @@ -62,62 +61,74 @@ interface INAVManager is ISnapshotHook { function navHook() external view returns (INAVHook); /// @notice Set the NAV hook contract that will receive NAV updates + /// @param poolId The pool ID /// @param navHook The address of the NAV hook contract - function setNAVHook(INAVHook navHook) external; + function setNAVHook(PoolId poolId, INAVHook navHook) external; /// @notice Check if an address can call management functions - function manager(address manager) external view returns (bool); + /// @param poolId The pool ID + /// @param manager The address of the manager + function manager(PoolId poolId, address manager) external view returns (bool); /// @notice Update whether an address can call management functions + /// @param poolId The pool ID /// @param manager The address of the manager /// @param canManage Whether the address can call management functions - function updateManager(address manager, bool canManage) external; + function updateManager(PoolId poolId, address manager, bool canManage) external; //---------------------------------------------------------------------------------------------- // Account creation //---------------------------------------------------------------------------------------------- /// @notice Initialize a new network by creating core accounts (equity, liability, gain, loss) + /// @param poolId The pool ID /// @param centrifugeId The Centrifuge ID of the network to initialize - function initializeNetwork(uint16 centrifugeId) external; + function initializeNetwork(PoolId poolId, uint16 centrifugeId) external; /// @notice Initialize a new holding asset account and associate it with the hub + /// @param poolId The pool ID /// @param scId The share class ID /// @param assetId The asset ID to initialize /// @param valuation The valuation contract for this asset - function initializeHolding(ShareClassId scId, AssetId assetId, IValuation valuation) external; + function initializeHolding(PoolId poolId, ShareClassId scId, AssetId assetId, IValuation valuation) external; /// @notice Initialize a new liability account and associate it with the hub + /// @param poolId The pool ID /// @param scId The share class ID /// @param assetId The asset ID to initialize as a liability /// @param valuation The valuation contract for this liability - function initializeLiability(ShareClassId scId, AssetId assetId, IValuation valuation) external; + function initializeLiability(PoolId poolId, ShareClassId scId, AssetId assetId, IValuation valuation) external; //---------------------------------------------------------------------------------------------- // Holding updates //---------------------------------------------------------------------------------------------- /// @notice Update the holding value for a specific asset + /// @param poolId The pool ID /// @param scId The share class ID /// @param assetId The asset ID to update - function updateHoldingValue(ShareClassId scId, AssetId assetId) external; + function updateHoldingValue(PoolId poolId, ShareClassId scId, AssetId assetId) external; /// @notice Update the valuation contract for a specific asset + /// @param poolId The pool ID /// @param scId The share class ID /// @param assetId The asset ID to update /// @param valuation The new valuation contract - function updateHoldingValuation(ShareClassId scId, AssetId assetId, IValuation valuation) external; + function updateHoldingValuation(PoolId poolId, ShareClassId scId, AssetId assetId, IValuation valuation) external; /// @notice Set the account ID for a specific asset holding + /// @param poolId The pool ID /// @param scId The share class ID /// @param assetId The asset ID /// @param kind The account kind /// @param accountId The account ID to set - function setHoldingAccountId(ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) external; + function setHoldingAccountId(PoolId poolId, ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) + external; /// @notice close gain/loss accounts by moving balances to equity account + /// @param poolId The pool ID /// @param centrifugeId The Centrifuge ID of the network - function closeGainLoss(uint16 centrifugeId) external; + function closeGainLoss(PoolId poolId, uint16 centrifugeId) external; //---------------------------------------------------------------------------------------------- // Calculations @@ -125,22 +136,23 @@ interface INAVManager is ISnapshotHook { /// @notice Calculate the net asset value for a specific network /// @dev NAV = equity + gain - loss - liability + /// @param poolId The pool ID /// @param centrifugeId The Centrifuge ID of the network - function netAssetValue(uint16 centrifugeId) external view returns (uint128); + function netAssetValue(PoolId poolId, uint16 centrifugeId) external view returns (uint128); //---------------------------------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------------------------------- /// @notice Get the asset account ID for a specific asset on a network - /// @param centrifugeId The Centrifuge ID of the network + /// @param poolId The pool ID /// @param assetId The asset ID - function assetAccount(uint16 centrifugeId, AssetId assetId) external view returns (AccountId); + function assetAccount(PoolId poolId, AssetId assetId) external view returns (AccountId); /// @notice Get the expense account ID for a specific asset on a network - /// @param centrifugeId The Centrifuge ID of the network + /// @param poolId The pool ID /// @param assetId The asset ID - function expenseAccount(uint16 centrifugeId, AssetId assetId) external view returns (AccountId); + function expenseAccount(PoolId poolId, AssetId assetId) external view returns (AccountId); /// @notice Get the equity account ID for a specific network /// @param centrifugeId The Centrifuge ID of the network diff --git a/src/managers/interfaces/INAVManagerFactory.sol b/src/managers/interfaces/INAVManagerFactory.sol deleted file mode 100644 index fbf0b000b..000000000 --- a/src/managers/interfaces/INAVManagerFactory.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.28; - -import {INAVManager} from "./INAVManager.sol"; - -import {PoolId} from "../../common/types/PoolId.sol"; - -interface INAVManagerFactory { - event DeployNavManager(PoolId indexed poolId, address indexed manager); - - error InvalidPoolId(); - - /// @notice Deploys new merkle proof manager. - function newManager(PoolId poolId) external returns (INAVManager); -} diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/interfaces/ISimplePriceManager.sol index 9302ec729..a1c02e734 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/interfaces/ISimplePriceManager.sol @@ -5,29 +5,33 @@ import {INAVHook} from "./INAVManager.sol"; import {D18} from "../../misc/types/D18.sol"; +import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; interface ISimplePriceManager is INAVHook { - event Update(uint128 newNAV, uint128 newIssuance, D18 newSharePrice); - event Transfer(uint16 indexed fromCentrifugeId, uint16 indexed toCentrifugeId, uint128 sharesTransferred); - event UpdateManager(address indexed manager, bool canManage); - event UpdateCaller(address indexed caller, bool canCall); + event Update(PoolId indexed poolId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); + event Transfer( + PoolId indexed poolId, uint16 indexed fromCentrifugeId, uint16 indexed toCentrifugeId, uint128 sharesTransferred + ); + event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); error InvalidShareClassCount(); - error InvalidPoolId(); - error InvalidShareClassId(); error MismatchedEpochs(); - error EmptyAddress(); - error NotAuthorized(); struct NetworkMetrics { uint128 netAssetValue; uint128 issuance; } - function globalIssuance() external view returns (uint128); - function globalNetAssetValue() external view returns (uint128); - function metrics(uint16 centrifugeId) external view returns (uint128 netAssetValue, uint128 issuance); + function globalIssuance(PoolId poolId) external view returns (uint128); + function globalNetAssetValue(PoolId poolId) external view returns (uint128); + function metrics(PoolId poolId, uint16 centrifugeId) + external + view + returns (uint128 netAssetValue, uint128 issuance); + function networks(PoolId poolId, uint256 index) external view returns (uint16); + function manager(PoolId poolId, address manager_) external view returns (bool); //---------------------------------------------------------------------------------------------- // Administration @@ -35,36 +39,45 @@ interface ISimplePriceManager is INAVHook { /// @notice Update the list of networks the pool is active on /// @dev Ensure the number of network updates can fit in a single block - function setNetworks(uint16[] calldata centrifugeIds) external; - - /// @notice Check if an address can manage the NAV manager - function manager(address manager) external view returns (bool); + /// @param poolId The pool ID + /// @param centrifugeIds Array of Centrifuge IDs for networks + function setNetworks(PoolId poolId, uint16[] calldata centrifugeIds) external; /// @notice Update whether an address can manage the NAV manager + /// @param poolId The pool ID /// @param manager The address of the manager /// @param canManage Whether the address can manage this manager - function updateManager(address manager, bool canManage) external; - - /// @notice Update whether an address can call NAVHook methods - /// @param caller The address of the caller - /// @param canCall Whether the address can call NAVHook methods - function updateCaller(address caller, bool canCall) external; + function updateManager(PoolId poolId, address manager, bool canManage) external; //---------------------------------------------------------------------------------------------- // Manager actions //---------------------------------------------------------------------------------------------- /// @notice Approve deposits and issue shares in sequence using current NAV per share + /// @param poolId The pool ID + /// @param scId The share class ID /// @param depositAssetId The asset ID for deposits /// @param approvedAssetAmount Amount of assets to approve /// @param extraGasLimit Extra gas limit for cross-chain operations - function approveDepositsAndIssueShares(AssetId depositAssetId, uint128 approvedAssetAmount, uint128 extraGasLimit) - external; + function approveDepositsAndIssueShares( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external; /// @notice Approve redeems and revoke shares in sequence using current NAV per share + /// @param poolId The pool ID + /// @param scId The share class ID /// @param payoutAssetId The asset ID for payouts /// @param approvedShareAmount Amount of shares to approve for redemption /// @param extraGasLimit Extra gas limit for cross-chain operations - function approveRedeemsAndRevokeShares(AssetId payoutAssetId, uint128 approvedShareAmount, uint128 extraGasLimit) - external; + function approveRedeemsAndRevokeShares( + PoolId poolId, + ShareClassId scId, + AssetId payoutAssetId, + uint128 approvedShareAmount, + uint128 extraGasLimit + ) external; } diff --git a/src/managers/interfaces/ISimplePriceManagerFactory.sol b/src/managers/interfaces/ISimplePriceManagerFactory.sol deleted file mode 100644 index 3bd979ef7..000000000 --- a/src/managers/interfaces/ISimplePriceManagerFactory.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.28; - -import {ISimplePriceManager} from "./ISimplePriceManager.sol"; - -import {PoolId} from "../../common/types/PoolId.sol"; - -interface ISimplePriceManagerFactory { - event DeploySimplePriceManager(PoolId indexed poolId, address indexed manager); - - error InvalidShareClassCount(); - - function newManager(PoolId poolId) external returns (ISimplePriceManager); -} diff --git a/test/managers/integration/NAVManager.t.sol b/test/managers/integration/NAVManager.t.sol index 6873d9bc5..d56fcabec 100644 --- a/test/managers/integration/NAVManager.t.sol +++ b/test/managers/integration/NAVManager.t.sol @@ -15,9 +15,6 @@ import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManage import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; contract NAVManagerIntegrationTest is BaseTest { - INAVManager public navManager; - ISimplePriceManager public priceManager; - PoolId constant POOL_A = PoolId.wrap(1); ShareClassId scId; @@ -60,22 +57,18 @@ contract NAVManagerIntegrationTest is BaseTest { vm.startPrank(FM); scId = hub.addShareClass(POOL_A, "Test Share Class", "TSC", bytes32("1")); - navManager = navManagerFactory.newManager(POOL_A); - priceManager = simplePriceManagerFactory.newManager(POOL_A); - hub.setSnapshotHook(POOL_A, ISnapshotHook(address(navManager))); hub.updateHubManager(POOL_A, address(navManager), true); - hub.updateHubManager(POOL_A, address(priceManager), true); - navManager.updateManager(manager, true); - priceManager.updateManager(manager, true); + hub.updateHubManager(POOL_A, address(simplePriceManager), true); + navManager.updateManager(POOL_A, manager, true); + simplePriceManager.updateManager(POOL_A, manager, true); - navManager.setNAVHook(INAVHook(address(priceManager))); - priceManager.updateCaller(address(navManager), true); + navManager.setNAVHook(POOL_A, INAVHook(address(simplePriceManager))); uint16[] memory networks = new uint16[](2); networks[0] = CHAIN_CP; networks[1] = CHAIN_CV; - priceManager.setNetworks(networks); + simplePriceManager.setNetworks(POOL_A, networks); vm.stopPrank(); @@ -84,19 +77,19 @@ contract NAVManagerIntegrationTest is BaseTest { valuation.setPrice(POOL_A, scId, asset3, d18(1, 1)); valuation.setPrice(POOL_A, scId, liabilityAsset, d18(1, 1)); - vm.deal(address(priceManager), 1 ether); + vm.deal(address(simplePriceManager), 1 ether); } /// forge-config: default.isolate = true function testSuccess() public { vm.startPrank(manager); - navManager.initializeNetwork(CHAIN_CP); - navManager.initializeNetwork(CHAIN_CV); + navManager.initializeNetwork(POOL_A, CHAIN_CP); + navManager.initializeNetwork(POOL_A, CHAIN_CV); - navManager.initializeHolding(scId, asset1, IValuation(address(valuation))); - navManager.initializeHolding(scId, asset2, IValuation(address(valuation))); - navManager.initializeHolding(scId, asset3, IValuation(address(valuation))); - navManager.initializeLiability(scId, liabilityAsset, IValuation(address(valuation))); + navManager.initializeHolding(POOL_A, scId, asset1, IValuation(address(valuation))); + navManager.initializeHolding(POOL_A, scId, asset2, IValuation(address(valuation))); + navManager.initializeHolding(POOL_A, scId, asset3, IValuation(address(valuation))); + navManager.initializeLiability(POOL_A, scId, liabilityAsset, IValuation(address(valuation))); cv.updateHoldingAmount(POOL_A, scId, asset1, uint128(1000 * 10 ** asset1Decimals), d18(1, 1), true, false, 0); cv.updateHoldingAmount(POOL_A, scId, asset2, uint128(2300 * 10 ** asset2Decimals), d18(1, 1), true, false, 1); @@ -120,12 +113,12 @@ contract NAVManagerIntegrationTest is BaseTest { vm.prank(address(root)); hub.updateShares(CHAIN_CP, POOL_A, scId, 500e18, true, true, 1); - uint128 navHub = navManager.netAssetValue(CHAIN_CP); - uint128 navSpoke = navManager.netAssetValue(CHAIN_CV); - (uint128 navHub2, uint128 issuanceHub) = priceManager.metrics(CHAIN_CP); - (uint128 navSpoke2, uint128 issuanceSpoke) = priceManager.metrics(CHAIN_CV); - uint128 globalNAV = priceManager.globalNetAssetValue(); - uint128 globalIssuance = priceManager.globalIssuance(); + uint128 navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); + uint128 navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); + (uint128 navHub2, uint128 issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); + (uint128 navSpoke2, uint128 issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); + uint128 globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); + uint128 globalIssuance = simplePriceManager.globalIssuance(POOL_A); assertEq(navHub, 500e18); assertEq(navSpoke, 3300e18); @@ -140,21 +133,21 @@ contract NAVManagerIntegrationTest is BaseTest { valuation.setPrice(POOL_A, scId, asset3, d18(1, 2)); // 50% decrease in value vm.prank(manager); - navManager.updateHoldingValue(scId, asset1); + navManager.updateHoldingValue(POOL_A, scId, asset1); vm.expectCall( address(hub), abi.encodeWithSelector(hub.updateSharePrice.selector, POOL_A, scId, d18(3650e18) / d18(3800e18)) ); vm.prank(manager); - navManager.updateHoldingValue(scId, asset3); - - navHub = navManager.netAssetValue(CHAIN_CP); - navSpoke = navManager.netAssetValue(CHAIN_CV); - (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); - (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); - globalNAV = priceManager.globalNetAssetValue(); - globalIssuance = priceManager.globalIssuance(); + navManager.updateHoldingValue(POOL_A, scId, asset3); + + navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); + navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); + (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); + globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); + globalIssuance = simplePriceManager.globalIssuance(POOL_A); (bool spokeGainIsPositive, uint128 spokeGain) = accounting.accountValue(POOL_A, navManager.gainAccount(CHAIN_CV)); (bool hubLossIsPositive, uint128 hubLoss) = accounting.accountValue(POOL_A, navManager.lossAccount(CHAIN_CP)); @@ -164,6 +157,7 @@ contract NAVManagerIntegrationTest is BaseTest { assertEq(hubLoss, 250e18); assertFalse(hubLossIsPositive); assertEq(navHub, 250e18); + assertEq(navSpoke, 3400e18); assertEq(navHub2, navHub); assertEq(navSpoke2, navSpoke); @@ -175,12 +169,12 @@ contract NAVManagerIntegrationTest is BaseTest { vm.prank(address(root)); hub.initiateTransferShares(CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0); - navHub = navManager.netAssetValue(CHAIN_CP); - navSpoke = navManager.netAssetValue(CHAIN_CV); - (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); - (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); - globalNAV = priceManager.globalNetAssetValue(); - globalIssuance = priceManager.globalIssuance(); + navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); + navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); + (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); + globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); + globalIssuance = simplePriceManager.globalIssuance(POOL_A); // NAV and global issuance should remain unchanged, only issuance per network changes assertEq(navHub, 250e18); @@ -200,12 +194,12 @@ contract NAVManagerIntegrationTest is BaseTest { vm.prank(address(root)); hub.updateHoldingAmount(CHAIN_CP, POOL_A, scId, liabilityAsset, 50e18, d18(1, 1), true, true, 2); - navHub = navManager.netAssetValue(CHAIN_CP); - navSpoke = navManager.netAssetValue(CHAIN_CV); - (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); - (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); - globalNAV = priceManager.globalNetAssetValue(); - globalIssuance = priceManager.globalIssuance(); + navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); + navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); + (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); + globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); + globalIssuance = simplePriceManager.globalIssuance(POOL_A); // Liability reduces the NAV assertEq(navHub, 200e18); @@ -225,12 +219,12 @@ contract NAVManagerIntegrationTest is BaseTest { CHAIN_CP, POOL_A, scId, asset3, uint128(100 * 10 ** asset3Decimals), d18(1, 2), false, true, 4 ); - navHub = navManager.netAssetValue(CHAIN_CP); - navSpoke = navManager.netAssetValue(CHAIN_CV); - (navHub2, issuanceHub) = priceManager.metrics(CHAIN_CP); - (navSpoke2, issuanceSpoke) = priceManager.metrics(CHAIN_CV); - globalNAV = priceManager.globalNetAssetValue(); - globalIssuance = priceManager.globalIssuance(); + navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); + navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); + (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); + globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); + globalIssuance = simplePriceManager.globalIssuance(POOL_A); // NAV should remain unchanged assertEq(navHub, 200e18); diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/unit/NAVManager.t.sol index 75a3eca9a..84ea1b605 100644 --- a/test/managers/unit/NAVManager.t.sol +++ b/test/managers/unit/NAVManager.t.sol @@ -17,9 +17,8 @@ import {IHoldings} from "../../../src/hub/interfaces/IHoldings.sol"; import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; -import {NAVManager, NAVManagerFactory} from "../../../src/managers/NAVManager.sol"; +import {NAVManager} from "../../../src/managers/NAVManager.sol"; import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; -import {INAVManagerFactory} from "../../../src/managers/interfaces/INAVManagerFactory.sol"; import "forge-std/Test.sol"; @@ -89,9 +88,12 @@ contract NAVManagerTest is Test { } function _deployManager() internal { - navManager = new NAVManager(POOL_A, IHub(hub)); + navManager = new NAVManager(IHub(hub), address(this)); + navManager.rely(hub); + navManager.rely(holdings); + vm.prank(hubManager); - navManager.updateManager(manager, true); + navManager.updateManager(POOL_A, manager, true); } function _mockAccountValue(AccountId accountId, uint128 value, bool isPositive) internal { @@ -105,7 +107,6 @@ contract NAVManagerTest is Test { contract NAVManagerConstructorTest is NAVManagerTest { function testConstructor() public view { - assertEq(navManager.poolId().raw(), POOL_A.raw()); assertEq(address(navManager.hub()), address(hub)); assertEq(address(navManager.holdings()), holdings); assertEq(address(navManager.accounting()), address(accounting)); @@ -116,10 +117,10 @@ contract NAVManagerConstructorTest is NAVManagerTest { contract NAVManagerConfigureTest is NAVManagerTest { function testSetNAVHookSuccess() public { vm.expectEmit(true, false, false, true); - emit INAVManager.SetNavHook(address(navHook)); + emit INAVManager.SetNavHook(POOL_A, address(navHook)); vm.prank(hubManager); - navManager.setNAVHook(navHook); + navManager.setNAVHook(POOL_A, navHook); assertEq(address(navManager.navHook()), address(navHook)); } @@ -127,12 +128,12 @@ contract NAVManagerConfigureTest is NAVManagerTest { function testSetNAVHookUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.setNAVHook(navHook); + navManager.setNAVHook(POOL_A, navHook); } function testSetNAVHookToZeroAddress() public { vm.prank(hubManager); - navManager.setNAVHook(INAVHook(address(0))); + navManager.setNAVHook(POOL_A, INAVHook(address(0))); assertEq(address(navManager.navHook()), address(0)); } @@ -141,28 +142,28 @@ contract NAVManagerConfigureTest is NAVManagerTest { address newManager = makeAddr("newManager"); vm.expectEmit(true, true, false, false); - emit INAVManager.UpdateManager(newManager, true); + emit INAVManager.UpdateManager(POOL_A, newManager, true); vm.prank(hubManager); - navManager.updateManager(newManager, true); + navManager.updateManager(POOL_A, newManager, true); - assertTrue(navManager.manager(newManager)); + assertTrue(navManager.manager(POOL_A, newManager)); } function testUpdateManagerRemove() public { address managerAddr = makeAddr("newManager"); vm.prank(hubManager); - navManager.updateManager(managerAddr, true); - assertTrue(navManager.manager(managerAddr)); + navManager.updateManager(POOL_A, managerAddr, true); + assertTrue(navManager.manager(POOL_A, managerAddr)); vm.expectEmit(true, true, false, false); - emit INAVManager.UpdateManager(managerAddr, false); + emit INAVManager.UpdateManager(POOL_A, managerAddr, false); vm.prank(hubManager); - navManager.updateManager(managerAddr, false); + navManager.updateManager(POOL_A, managerAddr, false); - assertFalse(navManager.manager(managerAddr)); + assertFalse(navManager.manager(POOL_A, managerAddr)); } function testUpdateManagerUnauthorized() public { @@ -170,13 +171,7 @@ contract NAVManagerConfigureTest is NAVManagerTest { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.updateManager(managerAddr, true); - } - - function testUpdateManagerZeroAddress() public { - vm.expectRevert(INAVManager.EmptyAddress.selector); - vm.prank(hubManager); - navManager.updateManager(address(0), true); + navManager.updateManager(POOL_A, managerAddr, true); } function testInitializeNetworkSuccess() public { @@ -202,27 +197,27 @@ contract NAVManagerConfigureTest is NAVManagerTest { ); vm.expectEmit(true, false, false, true); - emit INAVManager.InitializeNetwork(CENTRIFUGE_ID_1); + emit INAVManager.InitializeNetwork(POOL_A, CENTRIFUGE_ID_1); vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); - assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 5); + assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 5); } function testInitializeNetworkAlreadyInitialized() public { vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); vm.expectRevert(INAVManager.AlreadyInitialized.selector); vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); } function testInitializeNetworkUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); } } @@ -230,7 +225,7 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { function setUp() public override { super.setUp(); vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); } function testInitializeHoldingSuccess() public { @@ -255,24 +250,24 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { ); vm.expectEmit(true, true, false, true); - emit INAVManager.InitializeHolding(SC_1, asset1); + emit INAVManager.InitializeHolding(POOL_A, SC_1, asset1); vm.prank(manager); - navManager.initializeHolding(SC_1, asset1, mockValuation); + navManager.initializeHolding(POOL_A, SC_1, asset1, mockValuation); - assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 6); - assertEq(navManager.assetAccount(CENTRIFUGE_ID_1, asset1).raw(), expectedAssetAccount.raw()); + assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 6); + assertEq(navManager.assetAccount(POOL_A, asset1).raw(), expectedAssetAccount.raw()); } function testInitializeHoldingNotInitialized() public { vm.expectRevert(INAVManager.NotInitialized.selector); vm.prank(manager); - navManager.initializeHolding(SC_1, AssetId.wrap(uint128(3) << 64 | 300), mockValuation); + navManager.initializeHolding(POOL_A, SC_1, AssetId.wrap(uint128(3) << 64 | 300), mockValuation); } function testInitializeHoldingSameAssetTwice() public { vm.prank(manager); - navManager.initializeHolding(SC_1, asset1, mockValuation); + navManager.initializeHolding(POOL_A, SC_1, asset1, mockValuation); AccountId expectedAssetAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); @@ -281,16 +276,16 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { ); vm.prank(manager); - navManager.initializeHolding(SC_2, asset1, mockValuation); + navManager.initializeHolding(POOL_A, SC_2, asset1, mockValuation); // Account counter should increment again - assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 7); + assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 7); } function testInitializeHoldingUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.initializeHolding(SC_1, asset1, mockValuation); + navManager.initializeHolding(POOL_A, SC_1, asset1, mockValuation); } } @@ -298,7 +293,7 @@ contract NAVManagerLiabilityInitializationTest is NAVManagerTest { function setUp() public override { super.setUp(); vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); } function testInitializeLiabilitySuccess() public { @@ -321,25 +316,25 @@ contract NAVManagerLiabilityInitializationTest is NAVManagerTest { ); vm.expectEmit(true, true, false, true); - emit INAVManager.InitializeLiability(SC_1, asset1); + emit INAVManager.InitializeLiability(POOL_A, SC_1, asset1); vm.prank(manager); - navManager.initializeLiability(SC_1, asset1, mockValuation); + navManager.initializeLiability(POOL_A, SC_1, asset1, mockValuation); - assertEq(navManager.accountCounter(CENTRIFUGE_ID_1), 6); - assertEq(navManager.expenseAccount(CENTRIFUGE_ID_1, asset1).raw(), expectedExpenseAccount.raw()); + assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 6); + assertEq(navManager.expenseAccount(POOL_A, asset1).raw(), expectedExpenseAccount.raw()); } function testInitializeLiabilityNotInitialized() public { vm.expectRevert(INAVManager.NotInitialized.selector); vm.prank(manager); - navManager.initializeLiability(SC_1, asset2, mockValuation); + navManager.initializeLiability(POOL_A, SC_1, asset2, mockValuation); } function testInitializeLiabilityUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.initializeLiability(SC_1, asset1, mockValuation); + navManager.initializeLiability(POOL_A, SC_1, asset1, mockValuation); } } @@ -347,9 +342,9 @@ contract NAVManagerOnSyncTest is NAVManagerTest { function setUp() public override { super.setUp(); vm.prank(hubManager); - navManager.setNAVHook(navHook); + navManager.setNAVHook(POOL_A, navHook); vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); } function testOnSyncSuccess() public { @@ -365,19 +360,13 @@ contract NAVManagerOnSyncTest is NAVManagerTest { ); vm.expectEmit(true, true, false, true); - emit INAVManager.Sync(SC_1, CENTRIFUGE_ID_1, 1050); + emit INAVManager.Sync(POOL_A, SC_1, CENTRIFUGE_ID_1, 1050); vm.prank(holdings); navManager.onSync(POOL_A, SC_1, CENTRIFUGE_ID_1); } - function testOnSyncInvalidPoolId() public { - vm.expectRevert(INAVManager.InvalidPoolId.selector); - vm.prank(holdings); - navManager.onSync(POOL_B, SC_1, CENTRIFUGE_ID_1); - } - - function testOnSyncNotHoldings() public { + function testOnSyncNotAuthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); navManager.onSync(POOL_A, SC_1, CENTRIFUGE_ID_1); @@ -386,7 +375,7 @@ contract NAVManagerOnSyncTest is NAVManagerTest { function testOnSyncNoNAVHook() public { // Reset NAV hook to zero vm.prank(hubManager); - navManager.setNAVHook(INAVHook(address(0))); + navManager.setNAVHook(POOL_A, INAVHook(address(0))); vm.expectRevert(INAVManager.InvalidNAVHook.selector); vm.prank(holdings); @@ -403,12 +392,12 @@ contract NAVManagerNetAssetValueTest is NAVManagerTest { _mockAccountValue(navManager.lossAccount(CENTRIFUGE_ID_1), 100, false); _mockAccountValue(navManager.liabilityAccount(CENTRIFUGE_ID_1), 50, true); - uint128 nav = navManager.netAssetValue(CENTRIFUGE_ID_1); + uint128 nav = navManager.netAssetValue(POOL_A, CENTRIFUGE_ID_1); assertEq(nav, 1050); } function testNetAssetValueZero() public view { - uint128 nav = navManager.netAssetValue(CENTRIFUGE_ID_1); + uint128 nav = navManager.netAssetValue(POOL_A, CENTRIFUGE_ID_1); assertEq(nav, 0); } @@ -422,7 +411,7 @@ contract NAVManagerNetAssetValueTest is NAVManagerTest { _mockAccountValue(navManager.liabilityAccount(CENTRIFUGE_ID_1), 100, true); vm.expectRevert(); - navManager.netAssetValue(CENTRIFUGE_ID_1); + navManager.netAssetValue(POOL_A, CENTRIFUGE_ID_1); } } @@ -431,13 +420,13 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateHoldingValue.selector, POOL_A, SC_1, asset1)); vm.prank(manager); - navManager.updateHoldingValue(SC_1, asset1); + navManager.updateHoldingValue(POOL_A, SC_1, asset1); } function testUpdateHoldingValueUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.updateHoldingValue(SC_1, asset1); + navManager.updateHoldingValue(POOL_A, SC_1, asset1); } function testUpdateHoldingValuation() public { @@ -448,13 +437,13 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateHoldingValue.selector, POOL_A, SC_1, asset1)); vm.prank(manager); - navManager.updateHoldingValuation(SC_1, asset1, mockValuation); + navManager.updateHoldingValuation(POOL_A, SC_1, asset1, mockValuation); } function testUpdateHoldingValuationUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.updateHoldingValuation(SC_1, asset1, mockValuation); + navManager.updateHoldingValuation(POOL_A, SC_1, asset1, mockValuation); } function testSetHoldingAccountId() public { @@ -467,7 +456,7 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { ); vm.prank(manager); - navManager.setHoldingAccountId(SC_1, asset1, kind, accountId); + navManager.setHoldingAccountId(POOL_A, SC_1, asset1, kind, accountId); } function testSetHoldingAccountIdUnauthorized() public { @@ -476,7 +465,7 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.setHoldingAccountId(SC_1, asset1, kind, accountId); + navManager.setHoldingAccountId(POOL_A, SC_1, asset1, kind, accountId); } } @@ -484,7 +473,7 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { function setUp() public override { super.setUp(); vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); } function testCloseGainLossSuccess() public { @@ -511,7 +500,7 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); vm.prank(manager); - navManager.closeGainLoss(CENTRIFUGE_ID_1); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_1); } function testCloseGainLossOnlyGain() public { @@ -541,7 +530,7 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); vm.prank(manager); - navManager.closeGainLoss(CENTRIFUGE_ID_1); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_1); } function testCloseGainLossOnlyLoss() public { @@ -571,7 +560,7 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); vm.prank(manager); - navManager.closeGainLoss(CENTRIFUGE_ID_1); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_1); } function testCloseGainLossNoGainNoLoss() public { @@ -582,7 +571,7 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); vm.prank(manager); - navManager.closeGainLoss(CENTRIFUGE_ID_1); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_1); } function testCloseGainLossGainNotPositive() public { @@ -593,7 +582,7 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); vm.prank(manager); - navManager.closeGainLoss(CENTRIFUGE_ID_1); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_1); } function testCloseGainLossLossIsPositive() public { @@ -604,19 +593,19 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { vm.expectCall(accounting, abi.encodeWithSelector(IAccounting.lock.selector)); vm.prank(manager); - navManager.closeGainLoss(CENTRIFUGE_ID_1); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_1); } function testCloseGainLossNotInitialized() public { vm.expectRevert(INAVManager.NotInitialized.selector); vm.prank(manager); - navManager.closeGainLoss(CENTRIFUGE_ID_2); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_2); } function testCloseGainLossUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - navManager.closeGainLoss(CENTRIFUGE_ID_1); + navManager.closeGainLoss(POOL_A, CENTRIFUGE_ID_1); } } @@ -647,28 +636,28 @@ contract NAVManagerHelperFunctionsTest is NAVManagerTest { function testAssetAccount() public { vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); vm.prank(manager); - navManager.initializeHolding(SC_1, asset1, mockValuation); + navManager.initializeHolding(POOL_A, SC_1, asset1, mockValuation); AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); - AccountId actual = navManager.assetAccount(CENTRIFUGE_ID_1, asset1); + AccountId actual = navManager.assetAccount(POOL_A, asset1); assertEq(actual.raw(), expected.raw()); } function testExpenseAccount() public { vm.prank(manager); - navManager.initializeNetwork(CENTRIFUGE_ID_1); + navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); vm.prank(manager); - navManager.initializeLiability(SC_1, asset1, mockValuation); + navManager.initializeLiability(POOL_A, SC_1, asset1, mockValuation); AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); - AccountId actual = navManager.expenseAccount(CENTRIFUGE_ID_1, asset1); + AccountId actual = navManager.expenseAccount(POOL_A, asset1); assertEq(actual.raw(), expected.raw()); } function testAssetAccountNotInitialized() public view { - AccountId actual = navManager.assetAccount(CENTRIFUGE_ID_1, asset1); + AccountId actual = navManager.assetAccount(POOL_A, asset1); assertTrue(actual.isNull()); } } @@ -677,24 +666,18 @@ contract NAVManagerOnTransferTest is NAVManagerTest { function setUp() public override { super.setUp(); vm.prank(hubManager); - navManager.setNAVHook(navHook); + navManager.setNAVHook(POOL_A, navHook); } - function testOnTransferBasicAuth() public { - vm.expectRevert(INAVManager.NotAuthorized.selector); + function testOnTransferUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); navManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); } - function testOnTransferInvalidPoolId() public { - vm.expectRevert(INAVManager.InvalidPoolId.selector); - vm.prank(hub); - navManager.onTransfer(POOL_B, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); - } - function testOnTransferNoNAVHook() public { vm.prank(hubManager); - navManager.setNAVHook(INAVHook(address(0))); + navManager.setNAVHook(POOL_A, INAVHook(address(0))); vm.expectRevert(INAVManager.InvalidNAVHook.selector); vm.prank(hub); @@ -708,56 +691,9 @@ contract NAVManagerOnTransferTest is NAVManagerTest { ); vm.expectEmit(true, true, true, true); - emit INAVManager.Transfer(SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); + emit INAVManager.Transfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); vm.prank(hub); navManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 1); } } - -contract NAVManagerFactoryTest is Test { - address hub = address(new IsContract()); - address accounting = address(new IsContract()); - address holdings = address(new IsContract()); - address hubRegistry = address(new IsContract()); - - NAVManagerFactory factory; - - function setUp() public { - vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); - vm.mockCall(hub, abi.encodeWithSelector(IHub.accounting.selector), abi.encode(accounting)); - vm.mockCall(hub, abi.encodeWithSelector(IHub.holdings.selector), abi.encode(holdings)); - factory = new NAVManagerFactory(IHub(hub)); - } - - function testFactoryConstructor() public view { - assertEq(address(factory.hub()), address(hub)); - } - - function testNewManagerSuccess() public { - PoolId poolId = PoolId.wrap(1); - - vm.mockCall( - address(hubRegistry), abi.encodeWithSelector(IHubRegistry.exists.selector, poolId), abi.encode(true) - ); - - vm.expectEmit(true, false, false, false); - emit INAVManagerFactory.DeployNavManager(poolId, address(0)); - - INAVManager manager = factory.newManager(poolId); - - assertTrue(address(manager) != address(0)); - assertEq(NAVManager(address(manager)).poolId().raw(), poolId.raw()); - } - - function testNewManagerInvalidPool() public { - PoolId poolId = PoolId.wrap(1); - - vm.mockCall( - address(hubRegistry), abi.encodeWithSelector(IHubRegistry.exists.selector, poolId), abi.encode(false) - ); - - vm.expectRevert(INAVManagerFactory.InvalidPoolId.selector); - factory.newManager(poolId); - } -} diff --git a/test/managers/unit/SimplePriceManager.t.sol b/test/managers/unit/SimplePriceManager.t.sol index 5734d9ab0..e0854d5b2 100644 --- a/test/managers/unit/SimplePriceManager.t.sol +++ b/test/managers/unit/SimplePriceManager.t.sol @@ -14,8 +14,7 @@ import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; import {IShareClassManager} from "../../../src/hub/interfaces/IShareClassManager.sol"; import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; -import {ISimplePriceManagerFactory} from "../../../src/managers/interfaces/ISimplePriceManagerFactory.sol"; -import {SimplePriceManager, SimplePriceManagerFactory} from "../../../src/managers/SimplePriceManager.sol"; +import {SimplePriceManager} from "../../../src/managers/SimplePriceManager.sol"; import "forge-std/Test.sol"; @@ -114,12 +113,11 @@ contract SimplePriceManagerTest is Test { } function _deployManager() internal { - priceManager = new SimplePriceManager(POOL_A, IHub(hub)); - vm.prank(hubManager); - priceManager.updateManager(manager, true); + priceManager = new SimplePriceManager(IHub(hub), address(this)); + priceManager.rely(caller); vm.prank(hubManager); - priceManager.updateCaller(caller, true); + priceManager.updateManager(POOL_A, manager, true); vm.deal(address(priceManager), 1 ether); } @@ -127,24 +125,10 @@ contract SimplePriceManagerTest is Test { contract SimplePriceManagerConstructorTest is SimplePriceManagerTest { function testConstructorSuccess() public view { - assertEq(priceManager.poolId().raw(), POOL_A.raw()); - assertEq(priceManager.scId().raw(), SC_1.raw()); assertEq(address(priceManager.hub()), hub); assertEq(address(priceManager.shareClassManager()), shareClassManager); - assertEq(priceManager.globalIssuance(), 0); - assertEq(priceManager.globalNetAssetValue(), 0); - } - - function testConstructorInvalidShareClassCount() public { - vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_A), - abi.encode(2) - ); - - vm.expectRevert(ISimplePriceManager.InvalidShareClassCount.selector); - vm.prank(hubManager); - new SimplePriceManager(POOL_A, IHub(hub)); + assertEq(priceManager.globalIssuance(POOL_A), 0); + assertEq(priceManager.globalNetAssetValue(POOL_A), 0); } } @@ -156,11 +140,11 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { networks[2] = CENTRIFUGE_ID_3; vm.prank(hubManager); - priceManager.setNetworks(networks); + priceManager.setNetworks(POOL_A, networks); - assertEq(priceManager.networks(0), CENTRIFUGE_ID_1); - assertEq(priceManager.networks(1), CENTRIFUGE_ID_2); - assertEq(priceManager.networks(2), CENTRIFUGE_ID_3); + assertEq(priceManager.networks(POOL_A, 0), CENTRIFUGE_ID_1); + assertEq(priceManager.networks(POOL_A, 1), CENTRIFUGE_ID_2); + assertEq(priceManager.networks(POOL_A, 2), CENTRIFUGE_ID_3); } function testSetNetworksUnauthorized() public { @@ -169,42 +153,42 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - priceManager.setNetworks(networks); + priceManager.setNetworks(POOL_A, networks); } function testSetNetworksEmpty() public { uint16[] memory networks = new uint16[](0); vm.prank(hubManager); - priceManager.setNetworks(networks); + priceManager.setNetworks(POOL_A, networks); } function testUpdateManagerSuccess() public { address newManager = makeAddr("newManager"); vm.expectEmit(true, true, false, false); - emit ISimplePriceManager.UpdateManager(newManager, true); + emit ISimplePriceManager.UpdateManager(POOL_A, newManager, true); vm.prank(hubManager); - priceManager.updateManager(newManager, true); + priceManager.updateManager(POOL_A, newManager, true); - assertTrue(priceManager.manager(newManager)); + assertTrue(priceManager.manager(POOL_A, newManager)); } function testUpdateManagerRemove() public { address managerAddr = makeAddr("newManager"); vm.prank(hubManager); - priceManager.updateManager(managerAddr, true); - assertTrue(priceManager.manager(managerAddr)); + priceManager.updateManager(POOL_A, managerAddr, true); + assertTrue(priceManager.manager(POOL_A, managerAddr)); vm.expectEmit(true, true, false, false); - emit ISimplePriceManager.UpdateManager(managerAddr, false); + emit ISimplePriceManager.UpdateManager(POOL_A, managerAddr, false); vm.prank(hubManager); - priceManager.updateManager(managerAddr, false); + priceManager.updateManager(POOL_A, managerAddr, false); - assertFalse(priceManager.manager(managerAddr)); + assertFalse(priceManager.manager(POOL_A, managerAddr)); } function testUpdateManagerUnauthorized() public { @@ -212,55 +196,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - priceManager.updateManager(managerAddr, true); - } - - function testUpdateManagerZeroAddress() public { - vm.expectRevert(ISimplePriceManager.EmptyAddress.selector); - vm.prank(hubManager); - priceManager.updateManager(address(0), true); - } - - function testUpdateCallerSuccess() public { - address newCaller = makeAddr("newCaller"); - - vm.expectEmit(true, true, false, false); - emit ISimplePriceManager.UpdateCaller(newCaller, true); - - vm.prank(hubManager); - priceManager.updateCaller(newCaller, true); - - assertTrue(priceManager.caller(newCaller)); - } - - function testUpdateCallerRemove() public { - address callerAddr = makeAddr("newCaller"); - - vm.prank(hubManager); - priceManager.updateCaller(callerAddr, true); - assertTrue(priceManager.caller(callerAddr)); - - vm.expectEmit(true, true, false, false); - emit ISimplePriceManager.UpdateCaller(callerAddr, false); - - vm.prank(hubManager); - priceManager.updateCaller(callerAddr, false); - - assertFalse(priceManager.caller(callerAddr)); - } - - function testUpdateCallerUnauthorized() public { - address callerAddr = makeAddr("newCaller"); - - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.updateCaller(callerAddr, true); - } - - function testUpdateCallerZeroAddress() public { - vm.expectRevert(ISimplePriceManager.EmptyAddress.selector); - vm.prank(hubManager); - priceManager.updateCaller(address(0), true); + priceManager.updateManager(POOL_A, managerAddr, true); } } @@ -273,7 +209,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { networks[1] = CENTRIFUGE_ID_2; vm.prank(hubManager); - priceManager.setNetworks(networks); + priceManager.setNetworks(POOL_A, networks); } function testOnUpdateFirstUpdate() public { @@ -291,15 +227,15 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { ); vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(netAssetValue, 100, d18(10, 1)); + emit ISimplePriceManager.Update(POOL_A, netAssetValue, 100, d18(10, 1)); vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, netAssetValue); - assertEq(priceManager.globalIssuance(), 100); - assertEq(priceManager.globalNetAssetValue(), netAssetValue); + assertEq(priceManager.globalIssuance(POOL_A), 100); + assertEq(priceManager.globalNetAssetValue(POOL_A), netAssetValue); - (uint128 storedNAV, uint128 storedIssuance) = priceManager.metrics(CENTRIFUGE_ID_1); + (uint128 storedNAV, uint128 storedIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_1); assertEq(storedNAV, netAssetValue); assertEq(storedIssuance, 100); } @@ -314,13 +250,13 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateSharePrice.selector, POOL_A, SC_1, d18(9, 1))); vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 + emit ISimplePriceManager.Update(POOL_A, 2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, netAssetValue2); - assertEq(priceManager.globalIssuance(), 300); // 100 + 200 - assertEq(priceManager.globalNetAssetValue(), 2700); // 1000 + 1700 + assertEq(priceManager.globalIssuance(POOL_A), 300); // 100 + 200 + assertEq(priceManager.globalNetAssetValue(POOL_A), 2700); // 1000 + 1700 } function testOnUpdateExistingNetwork() public { @@ -336,25 +272,13 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { uint128 newNetAssetValue = 1200; vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(1200, 150, d18(8, 1)); // 1200/150 = 8 + emit ISimplePriceManager.Update(POOL_A, 1200, 150, d18(8, 1)); // 1200/150 = 8 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, newNetAssetValue); - assertEq(priceManager.globalIssuance(), 150); - assertEq(priceManager.globalNetAssetValue(), 1200); - } - - function testOnUpdateInvalidPoolId() public { - vm.expectRevert(ISimplePriceManager.InvalidPoolId.selector); - vm.prank(caller); - priceManager.onUpdate(POOL_B, SC_1, CENTRIFUGE_ID_1, 1000); - } - - function testOnUpdateInvalidShareClassId() public { - vm.expectRevert(ISimplePriceManager.InvalidShareClassId.selector); - vm.prank(caller); - priceManager.onUpdate(POOL_A, SC_2, CENTRIFUGE_ID_1, 1000); + assertEq(priceManager.globalIssuance(POOL_A), 150); + assertEq(priceManager.globalNetAssetValue(POOL_A), 1200); } function testOnUpdateUnauthorized() public { @@ -375,8 +299,8 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); - assertEq(priceManager.globalIssuance(), 0); - assertEq(priceManager.globalNetAssetValue(), 1000); + assertEq(priceManager.globalIssuance(POOL_A), 0); + assertEq(priceManager.globalNetAssetValue(POOL_A), 1000); } } @@ -395,13 +319,13 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { uint128 sharesTransferred = 50; vm.expectEmit(true, true, false, true); - emit ISimplePriceManager.Transfer(CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); + emit ISimplePriceManager.Transfer(POOL_A, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); - (uint128 fromNAV, uint128 fromIssuance) = priceManager.metrics(CENTRIFUGE_ID_1); - (uint128 toNAV, uint128 toIssuance) = priceManager.metrics(CENTRIFUGE_ID_2); + (uint128 fromNAV, uint128 fromIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_1); + (uint128 toNAV, uint128 toIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_2); assertEq(fromIssuance, 50); // 100 - 50 assertEq(toIssuance, 250); // 200 + 50 @@ -411,18 +335,6 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { assertEq(toNAV, 2000); } - function testOnTransferInvalidPoolId() public { - vm.expectRevert(ISimplePriceManager.InvalidPoolId.selector); - vm.prank(caller); - priceManager.onTransfer(POOL_B, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); - } - - function testOnTransferInvalidShareClassId() public { - vm.expectRevert(ISimplePriceManager.InvalidShareClassId.selector); - vm.prank(caller); - priceManager.onTransfer(POOL_A, SC_2, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); - } - function testOnTransferUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); @@ -433,8 +345,8 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 0); - (, uint128 fromIssuance) = priceManager.metrics(CENTRIFUGE_ID_1); - (, uint128 toIssuance) = priceManager.metrics(CENTRIFUGE_ID_2); + (, uint128 fromIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_1); + (, uint128 toIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_2); assertEq(fromIssuance, 100); assertEq(toIssuance, 200); @@ -466,13 +378,13 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { ); vm.prank(manager); - priceManager.approveDepositsAndIssueShares(asset1, approvedAssetAmount, extraGasLimit); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, approvedAssetAmount, extraGasLimit); } function testApproveDepositsAndIssueSharesUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - priceManager.approveDepositsAndIssueShares(asset1, 500, 100000); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); } function testApproveDepositsAndIssueSharesMismatchedEpochs() public { @@ -489,7 +401,7 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); vm.prank(manager); - priceManager.approveDepositsAndIssueShares(asset1, 500, 100000); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); } function testApproveRedeemsAndRevokeSharesSuccess() public { @@ -508,13 +420,13 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { ); vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(asset1, approvedShareAmount, extraGasLimit); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, approvedShareAmount, extraGasLimit); } function testApproveRedeemsAndRevokeSharesUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - priceManager.approveRedeemsAndRevokeShares(asset1, 50, 100000); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); } function testApproveRedeemsAndRevokeSharesMismatchedEpochs() public { @@ -531,66 +443,6 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(asset1, 50, 100000); - } -} - -contract SimplePriceManagerFactoryTest is Test { - PoolId constant POOL_A = PoolId.wrap(1); - PoolId constant POOL_B = PoolId.wrap(2); - ShareClassId constant SC_1 = ShareClassId.wrap(bytes16("1")); - - address hub = address(new IsContract()); - address shareClassManager = address(new IsContract()); - address hubRegistry = address(new IsContract()); - - SimplePriceManagerFactory factory; - - function setUp() public { - _setupMocks(); - factory = new SimplePriceManagerFactory(IHub(hub)); - } - - function _setupMocks() internal { - vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); - vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); - - vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_A), - abi.encode(1) - ); - vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.previewShareClassId.selector, POOL_A, 1), - abi.encode(SC_1) - ); - } - - function testFactoryConstructor() public view { - assertEq(address(factory.hub()), hub); - } - - function testNewManagerSuccess() public { - vm.expectEmit(true, false, true, true); - emit ISimplePriceManagerFactory.DeploySimplePriceManager(POOL_A, address(0)); - - ISimplePriceManager manager = factory.newManager(POOL_A); - - assertTrue(address(manager) != address(0)); - assertEq(SimplePriceManager(payable(address(manager))).poolId().raw(), POOL_A.raw()); - assertEq(SimplePriceManager(payable(address(manager))).scId().raw(), SC_1.raw()); - assertEq(address(SimplePriceManager(payable(address(manager))).hub()), hub); - assertEq(address(SimplePriceManager(payable(address(manager))).shareClassManager()), shareClassManager); - } - - function testNewManagerInvalidShareClassCount() public { - vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_B), - abi.encode(2) - ); - vm.expectRevert(ISimplePriceManagerFactory.InvalidShareClassCount.selector); - factory.newManager(POOL_B); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); } } From 6724eff78eea807a929b2bce7236c4972a1d7c66 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:21:23 +0200 Subject: [PATCH 53/83] split hub/spoke managers --- docs/architecture/deployers.puml | 10 ++- script/ExtendedHubDeployer.s.sol | 8 +- script/ExtendedSpokeDeployer.s.sol | 12 +-- script/HubDeployer.s.sol | 50 ++--------- script/HubManagersDeployer.s.sol | 85 +++++++++++++++++++ ...oyer.s.sol => SpokeManagersDeployer.s.sol} | 30 +++---- script/utils/README.md | 2 +- src/managers/{ => hub}/NAVManager.sol | 26 +++--- src/managers/{ => hub}/SimplePriceManager.sol | 24 +++--- .../{ => hub}/interfaces/INAVManager.sol | 12 +-- .../interfaces/ISimplePriceManager.sol | 8 +- .../{ => spoke}/MerkleProofManager.sol | 14 +-- src/managers/{ => spoke}/OnOfframpManager.sol | 18 ++-- src/managers/{ => spoke}/README.md | 0 .../{ => spoke}/decoders/BaseDecoder.sol | 4 +- .../{ => spoke}/decoders/CircleDecoder.sol | 0 .../{ => spoke}/decoders/VaultDecoder.sol | 0 .../interfaces/IBalanceSheetManager.sol | 2 +- .../interfaces/IMerkleProofManager.sol | 0 .../interfaces/IMerkleProofManagerFactory.sol | 2 +- .../interfaces/IOnOfframpManager.sol | 6 +- .../interfaces/IOnOfframpManagerFactory.sol | 4 +- test/hub/integration/BaseTest.sol | 9 +- test/managers/hub/Deployment.t.sol | 26 ++++++ .../{ => hub}/integration/NAVManager.t.sol | 26 +++--- test/managers/{ => hub}/unit/NAVManager.t.sol | 28 +++--- .../{ => hub}/unit/SimplePriceManager.t.sol | 22 ++--- test/managers/{ => spoke}/Deployment.t.sol | 12 +-- .../integration/MerkleProofManager.t.sol | 24 +++--- .../integration/OnOfframpManager.t.sol | 18 ++-- .../{ => spoke}/libraries/MerkleTreeLib.sol | 0 .../{ => spoke}/unit/OnOfframpManager.t.sol | 38 ++++----- 32 files changed, 304 insertions(+), 216 deletions(-) create mode 100644 script/HubManagersDeployer.s.sol rename script/{ManagersDeployer.s.sol => SpokeManagersDeployer.s.sol} (65%) rename src/managers/{ => hub}/NAVManager.sol (94%) rename src/managers/{ => hub}/SimplePriceManager.sol (91%) rename src/managers/{ => hub}/interfaces/INAVManager.sol (95%) rename src/managers/{ => hub}/interfaces/ISimplePriceManager.sol (93%) rename src/managers/{ => spoke}/MerkleProofManager.sol (91%) rename src/managers/{ => spoke}/OnOfframpManager.sol (90%) rename src/managers/{ => spoke}/README.md (100%) rename src/managers/{ => spoke}/decoders/BaseDecoder.sol (92%) rename src/managers/{ => spoke}/decoders/CircleDecoder.sol (100%) rename src/managers/{ => spoke}/decoders/VaultDecoder.sol (100%) rename src/managers/{ => spoke}/interfaces/IBalanceSheetManager.sol (84%) rename src/managers/{ => spoke}/interfaces/IMerkleProofManager.sol (100%) rename src/managers/{ => spoke}/interfaces/IMerkleProofManagerFactory.sol (88%) rename src/managers/{ => spoke}/interfaces/IOnOfframpManager.sol (80%) rename src/managers/{ => spoke}/interfaces/IOnOfframpManagerFactory.sol (76%) create mode 100644 test/managers/hub/Deployment.t.sol rename test/managers/{ => hub}/integration/NAVManager.t.sol (91%) rename test/managers/{ => hub}/unit/NAVManager.t.sol (96%) rename test/managers/{ => hub}/unit/SimplePriceManager.t.sol (95%) rename test/managers/{ => spoke}/Deployment.t.sol (61%) rename test/managers/{ => spoke}/integration/MerkleProofManager.t.sol (94%) rename test/managers/{ => spoke}/integration/OnOfframpManager.t.sol (86%) rename test/managers/{ => spoke}/libraries/MerkleTreeLib.sol (100%) rename test/managers/{ => spoke}/unit/OnOfframpManager.t.sol (93%) diff --git a/docs/architecture/deployers.puml b/docs/architecture/deployers.puml index 530c7e305..84373ed2d 100644 --- a/docs/architecture/deployers.puml +++ b/docs/architecture/deployers.puml @@ -12,9 +12,10 @@ class SpokeDeployer class AdaptersDeployer class ValuationsDeployer +class HubManagersDeployer class ExtendedHubDeployer -class ManagersDeployer +class SpokeManagersDeployer class VaultsDeployer class HooksDeployer class ExtendedSpokeDeployer @@ -32,14 +33,17 @@ HubDeployer -up-|> CommonDeployer SpokeDeployer -up-|> CommonDeployer AdaptersDeployer -up-|> CommonDeployer +HubManagersDeployer -up-|> HubDeployer ValuationsDeployer -up-|> HubDeployer + +ExtendedHubDeployer -up-|> HubManagersDeployer ExtendedHubDeployer -up-|> ValuationsDeployer -ManagersDeployer -up-|> SpokeDeployer +SpokeManagersDeployer -up-|> SpokeDeployer VaultsDeployer -up-|> SpokeDeployer HooksDeployer -up-|> SpokeDeployer -ExtendedSpokeDeployer -up-|> ManagersDeployer +ExtendedSpokeDeployer -up-|> SpokeManagersDeployer ExtendedSpokeDeployer -up-|> VaultsDeployer ExtendedSpokeDeployer -up-|> HooksDeployer diff --git a/script/ExtendedHubDeployer.s.sol b/script/ExtendedHubDeployer.s.sol index 5b62b990b..518f537fe 100644 --- a/script/ExtendedHubDeployer.s.sol +++ b/script/ExtendedHubDeployer.s.sol @@ -3,12 +3,13 @@ pragma solidity 0.8.28; import {CommonInput} from "./CommonDeployer.s.sol"; import {ValuationsDeployer, ValuationsActionBatcher} from "./ValuationsDeployer.s.sol"; +import {HubManagersDeployer, HubManagersActionBatcher} from "./HubManagersDeployer.s.sol"; import "forge-std/Script.sol"; -contract ExtendedHubActionBatcher is ValuationsActionBatcher {} +contract ExtendedHubActionBatcher is ValuationsActionBatcher, HubManagersActionBatcher {} -contract ExtendedHubDeployer is ValuationsDeployer { +contract ExtendedHubDeployer is ValuationsDeployer, HubManagersDeployer { function deployExtendedHub(CommonInput memory input, ExtendedHubActionBatcher batcher) public { _preDeployExtendedHub(input, batcher); _postDeployExtendedHub(batcher); @@ -16,13 +17,16 @@ contract ExtendedHubDeployer is ValuationsDeployer { function _preDeployExtendedHub(CommonInput memory input, ExtendedHubActionBatcher batcher) internal { _preDeployValuations(input, batcher); + _preDeployHubManagers(input, batcher); } function _postDeployExtendedHub(ExtendedHubActionBatcher batcher) internal { _postDeployValuations(batcher); + _postDeployHubManagers(batcher); } function removeExtendedHubDeployerAccess(ExtendedHubActionBatcher batcher) public { removeValuationsDeployerAccess(batcher); + removeHubManagersDeployerAccess(batcher); } } diff --git a/script/ExtendedSpokeDeployer.s.sol b/script/ExtendedSpokeDeployer.s.sol index 7958b9359..1440fdbed 100644 --- a/script/ExtendedSpokeDeployer.s.sol +++ b/script/ExtendedSpokeDeployer.s.sol @@ -4,13 +4,13 @@ pragma solidity 0.8.28; import {CommonInput} from "./CommonDeployer.s.sol"; import {HooksDeployer, HooksActionBatcher} from "./HooksDeployer.s.sol"; import {VaultsDeployer, VaultsActionBatcher} from "./VaultsDeployer.s.sol"; -import {ManagersDeployer, ManagersActionBatcher} from "./ManagersDeployer.s.sol"; +import {SpokeManagersDeployer, SpokeManagersActionBatcher} from "./SpokeManagersDeployer.s.sol"; import "forge-std/Script.sol"; -contract ExtendedSpokeActionBatcher is VaultsActionBatcher, HooksActionBatcher, ManagersActionBatcher {} +contract ExtendedSpokeActionBatcher is VaultsActionBatcher, HooksActionBatcher, SpokeManagersActionBatcher {} -contract ExtendedSpokeDeployer is VaultsDeployer, HooksDeployer, ManagersDeployer { +contract ExtendedSpokeDeployer is VaultsDeployer, HooksDeployer, SpokeManagersDeployer { function deployExtendedSpoke(CommonInput memory input, ExtendedSpokeActionBatcher batcher) public { _preDeployExtendedSpoke(input, batcher); _postDeployExtendedSpoke(batcher); @@ -19,18 +19,18 @@ contract ExtendedSpokeDeployer is VaultsDeployer, HooksDeployer, ManagersDeploye function _preDeployExtendedSpoke(CommonInput memory input, ExtendedSpokeActionBatcher batcher) internal { _preDeployVaults(input, batcher); _preDeployHooks(input, batcher); - _preDeployManagers(input, batcher); + _preDeploySpokeManagers(input, batcher); } function _postDeployExtendedSpoke(ExtendedSpokeActionBatcher batcher) internal { _postDeployVaults(batcher); _postDeployHooks(batcher); - _postDeployManagers(batcher); + _postDeploySpokeManagers(batcher); } function removeExtendedSpokeDeployerAccess(ExtendedSpokeActionBatcher batcher) public { removeVaultsDeployerAccess(batcher); removeHooksDeployerAccess(batcher); - removeManagersDeployerAccess(batcher); + removeSpokeManagersDeployerAccess(batcher); } } diff --git a/script/HubDeployer.s.sol b/script/HubDeployer.s.sol index 948106245..25374d39f 100644 --- a/script/HubDeployer.s.sol +++ b/script/HubDeployer.s.sol @@ -12,9 +12,6 @@ import {HubHelpers} from "../src/hub/HubHelpers.sol"; import {HubRegistry} from "../src/hub/HubRegistry.sol"; import {ShareClassManager} from "../src/hub/ShareClassManager.sol"; -import {NAVManager} from "../src/managers/NAVManager.sol"; -import {SimplePriceManager} from "../src/managers/SimplePriceManager.sol"; - import "forge-std/Script.sol"; abstract contract HubConstants { @@ -31,8 +28,6 @@ struct HubReport { ShareClassManager shareClassManager; HubHelpers hubHelpers; Hub hub; - NAVManager navManager; - SimplePriceManager simplePriceManager; } contract HubActionBatcher is CommonActionBatcher, HubConstants { @@ -47,8 +42,6 @@ contract HubActionBatcher is CommonActionBatcher, HubConstants { report.common.messageDispatcher.rely(address(report.hub)); report.common.multiAdapter.rely(address(report.hub)); report.common.poolEscrowFactory.rely(address(report.hub)); - report.navManager.rely(address(report.hub)); - report.simplePriceManager.rely(address(report.hub)); // Rely hub helpers report.accounting.rely(address(report.hubHelpers)); @@ -60,10 +53,6 @@ contract HubActionBatcher is CommonActionBatcher, HubConstants { report.hub.rely(address(report.common.messageDispatcher)); report.hub.rely(address(report.common.guardian)); - // Rely other - report.simplePriceManager.rely(address(report.navManager)); - report.navManager.rely(address(report.holdings)); - // Rely root report.hubRegistry.rely(address(report.common.root)); report.accounting.rely(address(report.common.root)); @@ -95,8 +84,6 @@ contract HubActionBatcher is CommonActionBatcher, HubConstants { report.shareClassManager.deny(address(this)); report.hub.deny(address(this)); report.hubHelpers.deny(address(this)); - report.navManager.deny(address(this)); - report.simplePriceManager.deny(address(this)); } } @@ -108,8 +95,6 @@ contract HubDeployer is CommonDeployer, HubConstants { ShareClassManager public shareClassManager; HubHelpers public hubHelpers; Hub public hub; - NAVManager public navManager; - SimplePriceManager public simplePriceManager; function deployHub(CommonInput memory input, HubActionBatcher batcher) public { _preDeployHub(input, batcher); @@ -117,6 +102,10 @@ contract HubDeployer is CommonDeployer, HubConstants { } function _preDeployHub(CommonInput memory input, HubActionBatcher batcher) internal { + if (address(hub) != address(0)) { + return; // Already deployed. Make this method idempotent. + } + _preDeployCommon(input, batcher); hubRegistry = HubRegistry( @@ -177,19 +166,6 @@ contract HubDeployer is CommonDeployer, HubConstants { ) ); - navManager = NAVManager( - create3( - generateSalt("navManager"), - abi.encodePacked(type(NAVManager).creationCode, abi.encode(hub, address(batcher))) - ) - ); - - address simplePriceManagerAddr = create3( - generateSalt("simplePriceManager"), - abi.encodePacked(type(SimplePriceManager).creationCode, abi.encode(hub, address(batcher))) - ); - simplePriceManager = SimplePriceManager(payable(simplePriceManagerAddr)); - batcher.engageHub(_hubReport()); register("hubRegistry", address(hubRegistry)); @@ -198,8 +174,6 @@ contract HubDeployer is CommonDeployer, HubConstants { register("shareClassManager", address(shareClassManager)); register("hubHelpers", address(hubHelpers)); register("hub", address(hub)); - register("navManager", address(navManager)); - register("simplePriceManager", address(simplePriceManager)); } function _postDeployHub(HubActionBatcher batcher) internal { @@ -207,21 +181,15 @@ contract HubDeployer is CommonDeployer, HubConstants { } function removeHubDeployerAccess(HubActionBatcher batcher) public { + if (hub.wards(address(batcher)) == 0) { + return; // Already removed. Make this method idempotent. + } + removeCommonDeployerAccess(batcher); batcher.revokeHub(_hubReport()); } function _hubReport() internal view returns (HubReport memory) { - return HubReport( - _commonReport(), - hubRegistry, - accounting, - holdings, - shareClassManager, - hubHelpers, - hub, - navManager, - simplePriceManager - ); + return HubReport(_commonReport(), hubRegistry, accounting, holdings, shareClassManager, hubHelpers, hub); } } diff --git a/script/HubManagersDeployer.s.sol b/script/HubManagersDeployer.s.sol new file mode 100644 index 000000000..0f5d515eb --- /dev/null +++ b/script/HubManagersDeployer.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {NAVManager} from "../src/managers/hub/NAVManager.sol"; +import {SimplePriceManager} from "../src/managers/hub/SimplePriceManager.sol"; + +import {CommonInput} from "./CommonDeployer.s.sol"; +import {HubDeployer, HubReport, HubActionBatcher} from "./HubDeployer.s.sol"; + +import "forge-std/Script.sol"; + +struct HubManagersReport { + HubReport hub; + NAVManager navManager; + SimplePriceManager simplePriceManager; +} + +contract HubManagersActionBatcher is HubActionBatcher { + function engageManagers(HubManagersReport memory report) public onlyDeployer { + // Rely hub + report.navManager.rely(address(report.hub.hub)); + report.simplePriceManager.rely(address(report.hub.hub)); + + // Rely other + report.simplePriceManager.rely(address(report.navManager)); + report.navManager.rely(address(report.hub.holdings)); + + // Rely root + report.navManager.rely(address(report.hub.common.root)); + report.simplePriceManager.rely(address(report.hub.common.root)); + + // File methods + } + + function revokeManagers(HubManagersReport memory report) public onlyDeployer { + report.navManager.deny(address(this)); + report.simplePriceManager.deny(address(this)); + } +} + +contract HubManagersDeployer is HubDeployer { + NAVManager public navManager; + SimplePriceManager public simplePriceManager; + + function deployHubManagers(CommonInput memory input, HubManagersActionBatcher batcher) public { + _preDeployHubManagers(input, batcher); + _postDeployHubManagers(batcher); + } + + function _preDeployHubManagers(CommonInput memory input, HubManagersActionBatcher batcher) internal { + _preDeployHub(input, batcher); + + navManager = NAVManager( + create3( + generateSalt("navManager"), + abi.encodePacked(type(NAVManager).creationCode, abi.encode(hub, address(batcher))) + ) + ); + + address simplePriceManagerAddr = create3( + generateSalt("simplePriceManager"), + abi.encodePacked(type(SimplePriceManager).creationCode, abi.encode(hub, address(batcher))) + ); + simplePriceManager = SimplePriceManager(payable(simplePriceManagerAddr)); + + batcher.engageManagers(_managersReport()); + + register("navManager", address(navManager)); + register("simplePriceManager", address(simplePriceManager)); + } + + function _postDeployHubManagers(HubManagersActionBatcher batcher) internal { + _postDeployHub(batcher); + } + + function removeHubManagersDeployerAccess(HubManagersActionBatcher batcher) public { + removeHubDeployerAccess(batcher); + + batcher.revokeManagers(_managersReport()); + } + + function _managersReport() internal view returns (HubManagersReport memory) { + return HubManagersReport(_hubReport(), navManager, simplePriceManager); + } +} diff --git a/script/ManagersDeployer.s.sol b/script/SpokeManagersDeployer.s.sol similarity index 65% rename from script/ManagersDeployer.s.sol rename to script/SpokeManagersDeployer.s.sol index 50bbc478b..a63a7d335 100644 --- a/script/ManagersDeployer.s.sol +++ b/script/SpokeManagersDeployer.s.sol @@ -4,14 +4,14 @@ pragma solidity 0.8.28; import {CommonInput} from "./CommonDeployer.s.sol"; import {SpokeDeployer, SpokeReport, SpokeActionBatcher} from "./SpokeDeployer.s.sol"; -import {VaultDecoder} from "../src/managers/decoders/VaultDecoder.sol"; -import {CircleDecoder} from "../src/managers/decoders/CircleDecoder.sol"; -import {OnOfframpManagerFactory} from "../src/managers/OnOfframpManager.sol"; -import {MerkleProofManagerFactory} from "../src/managers/MerkleProofManager.sol"; +import {VaultDecoder} from "../src/managers/spoke/decoders/VaultDecoder.sol"; +import {CircleDecoder} from "../src/managers/spoke/decoders/CircleDecoder.sol"; +import {OnOfframpManagerFactory} from "../src/managers/spoke/OnOfframpManager.sol"; +import {MerkleProofManagerFactory} from "../src/managers/spoke/MerkleProofManager.sol"; import "forge-std/Script.sol"; -struct ManagersReport { +struct SpokeManagersReport { SpokeReport spoke; OnOfframpManagerFactory onOfframpManagerFactory; MerkleProofManagerFactory merkleProofManagerFactory; @@ -19,20 +19,20 @@ struct ManagersReport { CircleDecoder circleDecoder; } -contract ManagersActionBatcher is SpokeActionBatcher {} +contract SpokeManagersActionBatcher is SpokeActionBatcher {} -contract ManagersDeployer is SpokeDeployer { +contract SpokeManagersDeployer is SpokeDeployer { OnOfframpManagerFactory public onOfframpManagerFactory; MerkleProofManagerFactory public merkleProofManagerFactory; VaultDecoder public vaultDecoder; CircleDecoder public circleDecoder; - function deployManagers(CommonInput memory input, ManagersActionBatcher batcher) public { - _preDeployManagers(input, batcher); - _postDeployManagers(batcher); + function deploySpokeManagers(CommonInput memory input, SpokeManagersActionBatcher batcher) public { + _preDeploySpokeManagers(input, batcher); + _postDeploySpokeManagers(batcher); } - function _preDeployManagers(CommonInput memory input, ManagersActionBatcher batcher) internal { + function _preDeploySpokeManagers(CommonInput memory input, SpokeManagersActionBatcher batcher) internal { _preDeploySpoke(input, batcher); onOfframpManagerFactory = OnOfframpManagerFactory( @@ -63,16 +63,16 @@ contract ManagersDeployer is SpokeDeployer { register("circleDecoder", address(circleDecoder)); } - function _postDeployManagers(ManagersActionBatcher batcher) internal { + function _postDeploySpokeManagers(SpokeManagersActionBatcher batcher) internal { _postDeploySpoke(batcher); } - function removeManagersDeployerAccess(ManagersActionBatcher batcher) public { + function removeSpokeManagersDeployerAccess(SpokeManagersActionBatcher batcher) public { removeSpokeDeployerAccess(batcher); } - function _managersReport() internal view returns (ManagersReport memory) { - return ManagersReport( + function _spokeManagersReport() internal view returns (SpokeManagersReport memory) { + return SpokeManagersReport( _spokeReport(), onOfframpManagerFactory, merkleProofManagerFactory, vaultDecoder, circleDecoder ); } diff --git a/script/utils/README.md b/script/utils/README.md index f84b76472..89517761a 100644 --- a/script/utils/README.md +++ b/script/utils/README.md @@ -175,7 +175,7 @@ import {Auth} from "../misc/Auth.sol"; // Before import {CommonInput} from "script/CommonDeployer.s.sol"; -// After (from script/ManagersDeployer.s.sol) +// After (from script/SpokeManagersDeployer.s.sol) import {CommonInput} from "./CommonDeployer.s.sol"; ``` diff --git a/src/managers/NAVManager.sol b/src/managers/hub/NAVManager.sol similarity index 94% rename from src/managers/NAVManager.sol rename to src/managers/hub/NAVManager.sol index c367bbbce..da7781a80 100644 --- a/src/managers/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -3,19 +3,19 @@ pragma solidity 0.8.28; import {INAVManager, INAVHook} from "./interfaces/INAVManager.sol"; -import {Auth} from "../misc/Auth.sol"; - -import {PoolId} from "../common/types/PoolId.sol"; -import {AssetId} from "../common/types/AssetId.sol"; -import {ShareClassId} from "../common/types/ShareClassId.sol"; -import {IValuation} from "../common/interfaces/IValuation.sol"; -import {ISnapshotHook} from "../common/interfaces/ISnapshotHook.sol"; -import {AccountId, withCentrifugeId} from "../common/types/AccountId.sol"; - -import {IHub} from "../hub/interfaces/IHub.sol"; -import {IHoldings} from "../hub/interfaces/IHoldings.sol"; -import {IAccounting} from "../hub/interfaces/IAccounting.sol"; -import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; +import {Auth} from "../../misc/Auth.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {IValuation} from "../../common/interfaces/IValuation.sol"; +import {ISnapshotHook} from "../../common/interfaces/ISnapshotHook.sol"; +import {AccountId, withCentrifugeId} from "../../common/types/AccountId.sol"; + +import {IHub} from "../../hub/interfaces/IHub.sol"; +import {IHoldings} from "../../hub/interfaces/IHoldings.sol"; +import {IAccounting} from "../../hub/interfaces/IAccounting.sol"; +import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is INAVManager, Auth { diff --git a/src/managers/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol similarity index 91% rename from src/managers/SimplePriceManager.sol rename to src/managers/hub/SimplePriceManager.sol index 5fa4880a7..83b08a7f5 100644 --- a/src/managers/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -4,18 +4,18 @@ pragma solidity 0.8.28; import {INAVHook} from "./interfaces/INAVManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; -import {D18, d18} from "../misc/types/D18.sol"; -import {IMulticall} from "../misc/interfaces/IMulticall.sol"; -import {Auth} from "../misc/Auth.sol"; - -import {PoolId} from "../common/types/PoolId.sol"; -import {AssetId} from "../common/types/AssetId.sol"; -import {ShareClassId} from "../common/types/ShareClassId.sol"; -import {MAX_MESSAGE_COST} from "../common/interfaces/IGasService.sol"; - -import {IHub} from "../hub/interfaces/IHub.sol"; -import {IHubRegistry} from "../hub/interfaces/IHubRegistry.sol"; -import {IShareClassManager} from "../hub/interfaces/IShareClassManager.sol"; +import {D18, d18} from "../../misc/types/D18.sol"; +import {IMulticall} from "../../misc/interfaces/IMulticall.sol"; +import {Auth} from "../../misc/Auth.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {MAX_MESSAGE_COST} from "../../common/interfaces/IGasService.sol"; + +import {IHub} from "../../hub/interfaces/IHub.sol"; +import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; +import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; /// @notice Share price calculation manager for single share class pools. contract SimplePriceManager is ISimplePriceManager, Auth { diff --git a/src/managers/interfaces/INAVManager.sol b/src/managers/hub/interfaces/INAVManager.sol similarity index 95% rename from src/managers/interfaces/INAVManager.sol rename to src/managers/hub/interfaces/INAVManager.sol index dcb1916f4..145050234 100644 --- a/src/managers/interfaces/INAVManager.sol +++ b/src/managers/hub/interfaces/INAVManager.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; -import {PoolId} from "../../common/types/PoolId.sol"; -import {AssetId} from "../../common/types/AssetId.sol"; -import {AccountId} from "../../common/types/AccountId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {IValuation} from "../../common/interfaces/IValuation.sol"; -import {ISnapshotHook} from "../../common/interfaces/ISnapshotHook.sol"; +import {PoolId} from "../../../common/types/PoolId.sol"; +import {AssetId} from "../../../common/types/AssetId.sol"; +import {AccountId} from "../../../common/types/AccountId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; +import {IValuation} from "../../../common/interfaces/IValuation.sol"; +import {ISnapshotHook} from "../../../common/interfaces/ISnapshotHook.sol"; interface INAVHook { /// @notice Callback when there is a new net asset value (NAV) on a specific network. diff --git a/src/managers/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol similarity index 93% rename from src/managers/interfaces/ISimplePriceManager.sol rename to src/managers/hub/interfaces/ISimplePriceManager.sol index a1c02e734..0b159268b 100644 --- a/src/managers/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.28; import {INAVHook} from "./INAVManager.sol"; -import {D18} from "../../misc/types/D18.sol"; +import {D18} from "../../../misc/types/D18.sol"; -import {PoolId} from "../../common/types/PoolId.sol"; -import {AssetId} from "../../common/types/AssetId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {PoolId} from "../../../common/types/PoolId.sol"; +import {AssetId} from "../../../common/types/AssetId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; interface ISimplePriceManager is INAVHook { event Update(PoolId indexed poolId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); diff --git a/src/managers/MerkleProofManager.sol b/src/managers/spoke/MerkleProofManager.sol similarity index 91% rename from src/managers/MerkleProofManager.sol rename to src/managers/spoke/MerkleProofManager.sol index 16ebb4d4a..2306234f2 100644 --- a/src/managers/MerkleProofManager.sol +++ b/src/managers/spoke/MerkleProofManager.sol @@ -4,15 +4,15 @@ pragma solidity 0.8.28; import {IMerkleProofManagerFactory} from "./interfaces/IMerkleProofManagerFactory.sol"; import {IMerkleProofManager, Call, PolicyLeaf} from "./interfaces/IMerkleProofManager.sol"; -import {CastLib} from "../misc/libraries/CastLib.sol"; -import {MerkleProofLib} from "../misc/libraries/MerkleProofLib.sol"; +import {CastLib} from "../../misc/libraries/CastLib.sol"; +import {MerkleProofLib} from "../../misc/libraries/MerkleProofLib.sol"; -import {PoolId} from "../common/types/PoolId.sol"; -import {ShareClassId} from "../common/types/ShareClassId.sol"; +import {PoolId} from "../../common/types/PoolId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {IBalanceSheet} from "../spoke/interfaces/IBalanceSheet.sol"; -import {IUpdateContract} from "../spoke/interfaces/IUpdateContract.sol"; -import {UpdateContractMessageLib, UpdateContractType} from "../spoke/libraries/UpdateContractMessageLib.sol"; +import {IBalanceSheet} from "../../spoke/interfaces/IBalanceSheet.sol"; +import {IUpdateContract} from "../../spoke/interfaces/IUpdateContract.sol"; +import {UpdateContractMessageLib, UpdateContractType} from "../../spoke/libraries/UpdateContractMessageLib.sol"; /// @title Merkle Proof Manager /// @author Inspired by Boring Vaults from Se7en-Seas diff --git a/src/managers/OnOfframpManager.sol b/src/managers/spoke/OnOfframpManager.sol similarity index 90% rename from src/managers/OnOfframpManager.sol rename to src/managers/spoke/OnOfframpManager.sol index 67a7b3169..e9a9ac7f2 100644 --- a/src/managers/OnOfframpManager.sol +++ b/src/managers/spoke/OnOfframpManager.sol @@ -5,17 +5,17 @@ import {IOnOfframpManager} from "./interfaces/IOnOfframpManager.sol"; import {IOnOfframpManagerFactory} from "./interfaces/IOnOfframpManagerFactory.sol"; import {IDepositManager, IWithdrawManager} from "./interfaces/IBalanceSheetManager.sol"; -import {CastLib} from "../misc/libraries/CastLib.sol"; -import {IERC165} from "../misc/interfaces/IERC165.sol"; -import {SafeTransferLib} from "../misc/libraries/SafeTransferLib.sol"; +import {CastLib} from "../../misc/libraries/CastLib.sol"; +import {IERC165} from "../../misc/interfaces/IERC165.sol"; +import {SafeTransferLib} from "../../misc/libraries/SafeTransferLib.sol"; -import {PoolId} from "../common/types/PoolId.sol"; -import {AssetId} from "../common/types/AssetId.sol"; -import {ShareClassId} from "../common/types/ShareClassId.sol"; +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {IBalanceSheet} from "../spoke/interfaces/IBalanceSheet.sol"; -import {IUpdateContract} from "../spoke/interfaces/IUpdateContract.sol"; -import {UpdateContractType, UpdateContractMessageLib} from "../spoke/libraries/UpdateContractMessageLib.sol"; +import {IBalanceSheet} from "../../spoke/interfaces/IBalanceSheet.sol"; +import {IUpdateContract} from "../../spoke/interfaces/IUpdateContract.sol"; +import {UpdateContractType, UpdateContractMessageLib} from "../../spoke/libraries/UpdateContractMessageLib.sol"; /// @title OnOfframpManager /// @notice Balance sheet manager for depositing and withdrawing ERC20 assets. diff --git a/src/managers/README.md b/src/managers/spoke/README.md similarity index 100% rename from src/managers/README.md rename to src/managers/spoke/README.md diff --git a/src/managers/decoders/BaseDecoder.sol b/src/managers/spoke/decoders/BaseDecoder.sol similarity index 92% rename from src/managers/decoders/BaseDecoder.sol rename to src/managers/spoke/decoders/BaseDecoder.sol index ea50d0b7c..e1b91598c 100644 --- a/src/managers/decoders/BaseDecoder.sol +++ b/src/managers/spoke/decoders/BaseDecoder.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.28; -import {PoolId} from "../../common/types/PoolId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {PoolId} from "../../../common/types/PoolId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; contract BaseDecoder { error FunctionNotImplemented(bytes _calldata); diff --git a/src/managers/decoders/CircleDecoder.sol b/src/managers/spoke/decoders/CircleDecoder.sol similarity index 100% rename from src/managers/decoders/CircleDecoder.sol rename to src/managers/spoke/decoders/CircleDecoder.sol diff --git a/src/managers/decoders/VaultDecoder.sol b/src/managers/spoke/decoders/VaultDecoder.sol similarity index 100% rename from src/managers/decoders/VaultDecoder.sol rename to src/managers/spoke/decoders/VaultDecoder.sol diff --git a/src/managers/interfaces/IBalanceSheetManager.sol b/src/managers/spoke/interfaces/IBalanceSheetManager.sol similarity index 84% rename from src/managers/interfaces/IBalanceSheetManager.sol rename to src/managers/spoke/interfaces/IBalanceSheetManager.sol index be1fcbb7c..69815ca1c 100644 --- a/src/managers/interfaces/IBalanceSheetManager.sol +++ b/src/managers/spoke/interfaces/IBalanceSheetManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; -import {IERC165} from "../../misc/interfaces/IERC165.sol"; +import {IERC165} from "../../../misc/interfaces/IERC165.sol"; interface IDepositManager is IERC165 { function deposit(address asset, uint256 tokenId, uint128 amount, address owner) external; diff --git a/src/managers/interfaces/IMerkleProofManager.sol b/src/managers/spoke/interfaces/IMerkleProofManager.sol similarity index 100% rename from src/managers/interfaces/IMerkleProofManager.sol rename to src/managers/spoke/interfaces/IMerkleProofManager.sol diff --git a/src/managers/interfaces/IMerkleProofManagerFactory.sol b/src/managers/spoke/interfaces/IMerkleProofManagerFactory.sol similarity index 88% rename from src/managers/interfaces/IMerkleProofManagerFactory.sol rename to src/managers/spoke/interfaces/IMerkleProofManagerFactory.sol index 9b6af2558..9d7497ffa 100644 --- a/src/managers/interfaces/IMerkleProofManagerFactory.sol +++ b/src/managers/spoke/interfaces/IMerkleProofManagerFactory.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.28; import {IMerkleProofManager} from "./IMerkleProofManager.sol"; -import {PoolId} from "../../common/types/PoolId.sol"; +import {PoolId} from "../../../common/types/PoolId.sol"; interface IMerkleProofManagerFactory { event DeployMerkleProofManager(PoolId indexed poolId, address indexed manager); diff --git a/src/managers/interfaces/IOnOfframpManager.sol b/src/managers/spoke/interfaces/IOnOfframpManager.sol similarity index 80% rename from src/managers/interfaces/IOnOfframpManager.sol rename to src/managers/spoke/interfaces/IOnOfframpManager.sol index 3661ab465..ecefe94cc 100644 --- a/src/managers/interfaces/IOnOfframpManager.sol +++ b/src/managers/spoke/interfaces/IOnOfframpManager.sol @@ -3,10 +3,10 @@ pragma solidity >=0.5.0; import {IDepositManager, IWithdrawManager} from "./IBalanceSheetManager.sol"; -import {PoolId} from "../../common/types/PoolId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {PoolId} from "../../../common/types/PoolId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; -import {IUpdateContract} from "../../spoke/interfaces/IUpdateContract.sol"; +import {IUpdateContract} from "../../../spoke/interfaces/IUpdateContract.sol"; interface IOnOfframpManager is IDepositManager, IWithdrawManager, IUpdateContract { event UpdateOnramp(address indexed asset, bool isEnabled); diff --git a/src/managers/interfaces/IOnOfframpManagerFactory.sol b/src/managers/spoke/interfaces/IOnOfframpManagerFactory.sol similarity index 76% rename from src/managers/interfaces/IOnOfframpManagerFactory.sol rename to src/managers/spoke/interfaces/IOnOfframpManagerFactory.sol index 338484cb5..1ec7c35ea 100644 --- a/src/managers/interfaces/IOnOfframpManagerFactory.sol +++ b/src/managers/spoke/interfaces/IOnOfframpManagerFactory.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.28; import {IOnOfframpManager} from "./IOnOfframpManager.sol"; -import {PoolId} from "../../common/types/PoolId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {PoolId} from "../../../common/types/PoolId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; interface IOnOfframpManagerFactory { event DeployOnOfframpManager(PoolId indexed poolId, ShareClassId scId, address indexed manager); diff --git a/test/hub/integration/BaseTest.sol b/test/hub/integration/BaseTest.sol index b9261c029..d2f596c24 100644 --- a/test/hub/integration/BaseTest.sol +++ b/test/hub/integration/BaseTest.sol @@ -12,12 +12,13 @@ import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; import {MAX_MESSAGE_COST} from "../../../src/common/interfaces/IGasService.sol"; import {HubDeployer, HubActionBatcher, CommonInput} from "../../../script/HubDeployer.s.sol"; +import {ExtendedHubDeployer, ExtendedHubActionBatcher} from "../../../script/ExtendedHubDeployer.s.sol"; import {MockVaults} from "../mocks/MockVaults.sol"; import "forge-std/Test.sol"; -contract BaseTest is HubDeployer, Test { +contract BaseTest is ExtendedHubDeployer, Test { uint16 constant CHAIN_CP = 5; uint16 constant CHAIN_CV = 6; @@ -77,11 +78,11 @@ contract BaseTest is HubDeployer, Test { version: bytes32(0) }); - HubActionBatcher batcher = new HubActionBatcher(); + ExtendedHubActionBatcher batcher = new ExtendedHubActionBatcher(); labelAddresses(""); - deployHub(input, batcher); + deployExtendedHub(input, batcher); _mockStuff(batcher); - removeHubDeployerAccess(batcher); + removeExtendedHubDeployerAccess(batcher); // Initialize accounts vm.deal(FM, 1 ether); diff --git a/test/managers/hub/Deployment.t.sol b/test/managers/hub/Deployment.t.sol new file mode 100644 index 000000000..29fc4085d --- /dev/null +++ b/test/managers/hub/Deployment.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {CommonDeploymentInputTest} from "../../common/Deployment.t.sol"; + +import {HubManagersDeployer, HubManagersActionBatcher} from "../../../script/HubManagersDeployer.s.sol"; + +import "forge-std/Test.sol"; + +contract ManagersDeploymentTest is HubManagersDeployer, CommonDeploymentInputTest { + function setUp() public { + HubManagersActionBatcher batcher = new HubManagersActionBatcher(); + deployHubManagers(_commonInput(), batcher); + removeHubManagersDeployerAccess(batcher); + } + + function testNavManager() public view { + // dependencies set correctly + assertEq(address(navManager.hub()), address(hub)); + } + + function testSimplePriceManager() public view { + // dependencies set correctly + assertEq(address(simplePriceManager.hub()), address(hub)); + } +} diff --git a/test/managers/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol similarity index 91% rename from test/managers/integration/NAVManager.t.sol rename to test/managers/hub/integration/NAVManager.t.sol index d56fcabec..abbed1639 100644 --- a/test/managers/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {d18} from "../../../src/misc/types/D18.sol"; +import {d18} from "../../../../src/misc/types/D18.sol"; -import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; -import {IValuation} from "../../../src/common/interfaces/IValuation.sol"; -import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; -import {ISnapshotHook} from "../../../src/common/interfaces/ISnapshotHook.sol"; +import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; +import {IValuation} from "../../../../src/common/interfaces/IValuation.sol"; +import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; +import {ISnapshotHook} from "../../../../src/common/interfaces/ISnapshotHook.sol"; -import "../../hub/integration/BaseTest.sol"; +import "../../../hub/integration/BaseTest.sol"; -import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; -import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; +import {INAVManager, INAVHook} from "../../../../src/managers/hub/interfaces/INAVManager.sol"; +import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; contract NAVManagerIntegrationTest is BaseTest { PoolId constant POOL_A = PoolId.wrap(1); @@ -154,9 +154,9 @@ contract NAVManagerIntegrationTest is BaseTest { assertEq(spokeGain, 100e18); assertTrue(spokeGainIsPositive); - assertEq(hubLoss, 250e18); + assertEq(hubLoss, 250e18, "hubLoss2"); assertFalse(hubLossIsPositive); - assertEq(navHub, 250e18); + assertEq(navHub, 250e18, "navHub2"); assertEq(navSpoke, 3400e18); assertEq(navHub2, navHub); @@ -177,9 +177,9 @@ contract NAVManagerIntegrationTest is BaseTest { globalIssuance = simplePriceManager.globalIssuance(POOL_A); // NAV and global issuance should remain unchanged, only issuance per network changes - assertEq(navHub, 250e18); + assertEq(navHub, 250e18, "navHub3"); assertEq(navSpoke, 3400e18); - assertEq(navHub2, navHub); + assertEq(navHub2, navHub, "navHub v navHub3"); assertEq(navSpoke2, navSpoke); assertEq(issuanceHub, 370e18); assertEq(issuanceSpoke, 3430e18); diff --git a/test/managers/unit/NAVManager.t.sol b/test/managers/hub/unit/NAVManager.t.sol similarity index 96% rename from test/managers/unit/NAVManager.t.sol rename to test/managers/hub/unit/NAVManager.t.sol index 84ea1b605..7faebde01 100644 --- a/test/managers/unit/NAVManager.t.sol +++ b/test/managers/hub/unit/NAVManager.t.sol @@ -1,24 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {d18} from "../../../src/misc/types/D18.sol"; -import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; +import {d18} from "../../../../src/misc/types/D18.sol"; +import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; -import {Mock} from "../../common/mocks/Mock.sol"; -import {MockValuation} from "../../common/mocks/MockValuation.sol"; +import {Mock} from "../../../common/mocks/Mock.sol"; +import {MockValuation} from "../../../common/mocks/MockValuation.sol"; -import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; -import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; -import {AccountId, withCentrifugeId} from "../../../src/common/types/AccountId.sol"; +import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; +import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; +import {AccountId, withCentrifugeId} from "../../../../src/common/types/AccountId.sol"; -import {IHub} from "../../../src/hub/interfaces/IHub.sol"; -import {IHoldings} from "../../../src/hub/interfaces/IHoldings.sol"; -import {IAccounting} from "../../../src/hub/interfaces/IAccounting.sol"; -import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; +import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; +import {IHoldings} from "../../../../src/hub/interfaces/IHoldings.sol"; +import {IAccounting} from "../../../../src/hub/interfaces/IAccounting.sol"; +import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; -import {NAVManager} from "../../../src/managers/NAVManager.sol"; -import {INAVManager, INAVHook} from "../../../src/managers/interfaces/INAVManager.sol"; +import {NAVManager} from "../../../../src/managers/hub/NAVManager.sol"; +import {INAVManager, INAVHook} from "../../../../src/managers/hub/interfaces/INAVManager.sol"; import "forge-std/Test.sol"; diff --git a/test/managers/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol similarity index 95% rename from test/managers/unit/SimplePriceManager.t.sol rename to test/managers/hub/unit/SimplePriceManager.t.sol index e0854d5b2..6116f6cee 100644 --- a/test/managers/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -1,20 +1,20 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {D18, d18} from "../../../src/misc/types/D18.sol"; -import {Multicall} from "../../../src/misc/Multicall.sol"; -import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; +import {D18, d18} from "../../../../src/misc/types/D18.sol"; +import {Multicall} from "../../../../src/misc/Multicall.sol"; +import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; -import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; -import {AssetId, newAssetId} from "../../../src/common/types/AssetId.sol"; +import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; +import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; -import {IHub} from "../../../src/hub/interfaces/IHub.sol"; -import {IHubRegistry} from "../../../src/hub/interfaces/IHubRegistry.sol"; -import {IShareClassManager} from "../../../src/hub/interfaces/IShareClassManager.sol"; +import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; +import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; +import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; -import {ISimplePriceManager} from "../../../src/managers/interfaces/ISimplePriceManager.sol"; -import {SimplePriceManager} from "../../../src/managers/SimplePriceManager.sol"; +import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; +import {SimplePriceManager} from "../../../../src/managers/hub/SimplePriceManager.sol"; import "forge-std/Test.sol"; diff --git a/test/managers/Deployment.t.sol b/test/managers/spoke/Deployment.t.sol similarity index 61% rename from test/managers/Deployment.t.sol rename to test/managers/spoke/Deployment.t.sol index e902208c1..965db2e6a 100644 --- a/test/managers/Deployment.t.sol +++ b/test/managers/spoke/Deployment.t.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {CommonDeploymentInputTest} from "../common/Deployment.t.sol"; +import {CommonDeploymentInputTest} from "../../common/Deployment.t.sol"; -import {ManagersDeployer, ManagersActionBatcher} from "../../script/ManagersDeployer.s.sol"; +import {SpokeManagersDeployer, SpokeManagersActionBatcher} from "../../../script/SpokeManagersDeployer.s.sol"; import "forge-std/Test.sol"; -contract ManagersDeploymentTest is ManagersDeployer, CommonDeploymentInputTest { +contract ManagersDeploymentTest is SpokeManagersDeployer, CommonDeploymentInputTest { function setUp() public { - ManagersActionBatcher batcher = new ManagersActionBatcher(); - deployManagers(_commonInput(), batcher); - removeManagersDeployerAccess(batcher); + SpokeManagersActionBatcher batcher = new SpokeManagersActionBatcher(); + deploySpokeManagers(_commonInput(), batcher); + removeSpokeManagersDeployerAccess(batcher); } function testOnOfframpManagerFactory() public view { diff --git a/test/managers/integration/MerkleProofManager.t.sol b/test/managers/spoke/integration/MerkleProofManager.t.sol similarity index 94% rename from test/managers/integration/MerkleProofManager.t.sol rename to test/managers/spoke/integration/MerkleProofManager.t.sol index 80beb42df..684c0f4f4 100644 --- a/test/managers/integration/MerkleProofManager.t.sol +++ b/test/managers/spoke/integration/MerkleProofManager.t.sol @@ -1,23 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {IAuth} from "../../../src/misc/Auth.sol"; -import {D18, d18} from "../../../src/misc/types/D18.sol"; -import {CastLib} from "../../../src/misc/libraries/CastLib.sol"; +import {IAuth} from "../../../../src/misc/Auth.sol"; +import {D18, d18} from "../../../../src/misc/types/D18.sol"; +import {CastLib} from "../../../../src/misc/libraries/CastLib.sol"; -import {AssetId} from "../../../src/common/types/AssetId.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; +import {AssetId} from "../../../../src/common/types/AssetId.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; -import "../../spoke/integration/BaseTest.sol"; +import "../../../spoke/integration/BaseTest.sol"; -import {BalanceSheet} from "../../../src/spoke/BalanceSheet.sol"; -import {UpdateContractMessageLib} from "../../../src/spoke/libraries/UpdateContractMessageLib.sol"; +import {BalanceSheet} from "../../../../src/spoke/BalanceSheet.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; -import {UpdateRestrictionMessageLib} from "../../../src/hooks/libraries/UpdateRestrictionMessageLib.sol"; +import {UpdateRestrictionMessageLib} from "../../../../src/hooks/libraries/UpdateRestrictionMessageLib.sol"; -import {VaultDecoder} from "../../../src/managers/decoders/VaultDecoder.sol"; -import {MerkleProofManager, PolicyLeaf, Call} from "../../../src/managers/MerkleProofManager.sol"; -import {IMerkleProofManager, IERC7751} from "../../../src/managers/interfaces/IMerkleProofManager.sol"; +import {VaultDecoder} from "../../../../src/managers/spoke/decoders/VaultDecoder.sol"; +import {MerkleProofManager, PolicyLeaf, Call} from "../../../../src/managers/spoke/MerkleProofManager.sol"; +import {IMerkleProofManager, IERC7751} from "../../../../src/managers/spoke/interfaces/IMerkleProofManager.sol"; import {MerkleTreeLib} from "../libraries/MerkleTreeLib.sol"; diff --git a/test/managers/integration/OnOfframpManager.t.sol b/test/managers/spoke/integration/OnOfframpManager.t.sol similarity index 86% rename from test/managers/integration/OnOfframpManager.t.sol rename to test/managers/spoke/integration/OnOfframpManager.t.sol index cb4d6adc1..4b08a057f 100644 --- a/test/managers/integration/OnOfframpManager.t.sol +++ b/test/managers/spoke/integration/OnOfframpManager.t.sol @@ -1,20 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {D18, d18} from "../../../src/misc/types/D18.sol"; -import {CastLib} from "../../../src/misc/libraries/CastLib.sol"; +import {D18, d18} from "../../../../src/misc/types/D18.sol"; +import {CastLib} from "../../../../src/misc/libraries/CastLib.sol"; -import {AssetId} from "../../../src/common/types/AssetId.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; +import {AssetId} from "../../../../src/common/types/AssetId.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; -import "../../spoke/integration/BaseTest.sol"; +import "../../../spoke/integration/BaseTest.sol"; -import {UpdateContractMessageLib} from "../../../src/spoke/libraries/UpdateContractMessageLib.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; -import {UpdateRestrictionMessageLib} from "../../../src/hooks/libraries/UpdateRestrictionMessageLib.sol"; +import {UpdateRestrictionMessageLib} from "../../../../src/hooks/libraries/UpdateRestrictionMessageLib.sol"; -import {OnOfframpManagerFactory} from "../../../src/managers/OnOfframpManager.sol"; -import {IOnOfframpManager} from "../../../src/managers/interfaces/IOnOfframpManager.sol"; +import {OnOfframpManagerFactory} from "../../../../src/managers/spoke/OnOfframpManager.sol"; +import {IOnOfframpManager} from "../../../../src/managers/spoke/interfaces/IOnOfframpManager.sol"; abstract contract OnOfframpManagerBaseTest is BaseTest { using CastLib for *; diff --git a/test/managers/libraries/MerkleTreeLib.sol b/test/managers/spoke/libraries/MerkleTreeLib.sol similarity index 100% rename from test/managers/libraries/MerkleTreeLib.sol rename to test/managers/spoke/libraries/MerkleTreeLib.sol diff --git a/test/managers/unit/OnOfframpManager.t.sol b/test/managers/spoke/unit/OnOfframpManager.t.sol similarity index 93% rename from test/managers/unit/OnOfframpManager.t.sol rename to test/managers/spoke/unit/OnOfframpManager.t.sol index 27462be47..053145c18 100644 --- a/test/managers/unit/OnOfframpManager.t.sol +++ b/test/managers/spoke/unit/OnOfframpManager.t.sol @@ -1,25 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; -import {IERC20} from "../../../src/misc/interfaces/IERC20.sol"; -import {CastLib} from "../../../src/misc/libraries/CastLib.sol"; -import {IERC165} from "../../../src/misc/interfaces/IERC165.sol"; -import {IEscrow} from "../../../src/misc/interfaces/IEscrow.sol"; -import {IERC7751} from "../../../src/misc/interfaces/IERC7751.sol"; - -import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {AssetId} from "../../../src/common/types/AssetId.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; - -import {ISpoke} from "../../../src/spoke/interfaces/ISpoke.sol"; -import {IBalanceSheet} from "../../../src/spoke/interfaces/IBalanceSheet.sol"; -import {IUpdateContract} from "../../../src/spoke/interfaces/IUpdateContract.sol"; -import {UpdateContractMessageLib} from "../../../src/spoke/libraries/UpdateContractMessageLib.sol"; - -import {OnOfframpManagerFactory} from "../../../src/managers/OnOfframpManager.sol"; -import {IOnOfframpManager} from "../../../src/managers/interfaces/IOnOfframpManager.sol"; -import {IDepositManager, IWithdrawManager} from "../../../src/managers/interfaces/IBalanceSheetManager.sol"; +import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; +import {IERC20} from "../../../../src/misc/interfaces/IERC20.sol"; +import {CastLib} from "../../../../src/misc/libraries/CastLib.sol"; +import {IERC165} from "../../../../src/misc/interfaces/IERC165.sol"; +import {IEscrow} from "../../../../src/misc/interfaces/IEscrow.sol"; +import {IERC7751} from "../../../../src/misc/interfaces/IERC7751.sol"; + +import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {AssetId} from "../../../../src/common/types/AssetId.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; + +import {ISpoke} from "../../../../src/spoke/interfaces/ISpoke.sol"; +import {IBalanceSheet} from "../../../../src/spoke/interfaces/IBalanceSheet.sol"; +import {IUpdateContract} from "../../../../src/spoke/interfaces/IUpdateContract.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; + +import {OnOfframpManagerFactory} from "../../../../src/managers/spoke/OnOfframpManager.sol"; +import {IOnOfframpManager} from "../../../../src/managers/spoke/interfaces/IOnOfframpManager.sol"; +import {IDepositManager, IWithdrawManager} from "../../../../src/managers/spoke/interfaces/IBalanceSheetManager.sol"; import "forge-std/Test.sol"; From 764dc88d7985d19360d910a66d84657fee50b94a Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:21:58 +0200 Subject: [PATCH 54/83] comment --- script/HubManagersDeployer.s.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/script/HubManagersDeployer.s.sol b/script/HubManagersDeployer.s.sol index 0f5d515eb..a747e2d92 100644 --- a/script/HubManagersDeployer.s.sol +++ b/script/HubManagersDeployer.s.sol @@ -28,8 +28,6 @@ contract HubManagersActionBatcher is HubActionBatcher { // Rely root report.navManager.rely(address(report.hub.common.root)); report.simplePriceManager.rely(address(report.hub.common.root)); - - // File methods } function revokeManagers(HubManagersReport memory report) public onlyDeployer { From 88ea6b0f03f30c152146cd7270b73ff726cadfdd Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:21:40 +0200 Subject: [PATCH 55/83] account id uint256 --- src/common/types/AccountId.sol | 12 ++- src/managers/hub/NAVManager.sol | 73 ++++++++----------- src/managers/hub/SimplePriceManager.sol | 37 ++++++---- src/managers/hub/interfaces/INAVManager.sol | 14 ++-- .../hub/interfaces/ISimplePriceManager.sol | 13 +++- .../managers/hub/integration/NAVManager.t.sol | 35 ++++----- test/managers/hub/unit/NAVManager.t.sol | 40 ++++------ .../hub/unit/SimplePriceManager.t.sol | 48 +++++++----- 8 files changed, 136 insertions(+), 136 deletions(-) diff --git a/src/common/types/AccountId.sol b/src/common/types/AccountId.sol index 355c50dd0..1ea33bee6 100644 --- a/src/common/types/AccountId.sol +++ b/src/common/types/AccountId.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -type AccountId is uint32; +import {AssetId} from "./AssetId.sol"; -function raw(AccountId accountId_) pure returns (uint32) { +type AccountId is uint256; + +function raw(AccountId accountId_) pure returns (uint256) { return AccountId.unwrap(accountId_); } @@ -16,7 +18,11 @@ function isNull(AccountId accountId) pure returns (bool) { } function withCentrifugeId(uint16 centrifugeId, uint16 index) pure returns (AccountId) { - return AccountId.wrap((uint32(centrifugeId) << 16) | uint32(index)); + return AccountId.wrap((uint256(centrifugeId) << 16) | uint256(index)); +} + +function withAssetId(AssetId assetId, uint16 index) pure returns (AccountId) { + return AccountId.wrap((uint256(assetId.raw()) << 16) | uint256(index)); } using {raw, neq as !=, isNull} for AccountId global; diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index da7781a80..bbd79be44 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -10,23 +10,22 @@ import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {IValuation} from "../../common/interfaces/IValuation.sol"; import {ISnapshotHook} from "../../common/interfaces/ISnapshotHook.sol"; -import {AccountId, withCentrifugeId} from "../../common/types/AccountId.sol"; +import {AccountId, withCentrifugeId, withAssetId} from "../../common/types/AccountId.sol"; -import {IHub} from "../../hub/interfaces/IHub.sol"; +import {IHub, AccountType} from "../../hub/interfaces/IHub.sol"; import {IHoldings} from "../../hub/interfaces/IHoldings.sol"; import {IAccounting} from "../../hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is INAVManager, Auth { - IHub public immutable hub; - IHubRegistry public immutable hubRegistry; - IHoldings public immutable holdings; - IAccounting public immutable accounting; - - INAVHook public navHook; - mapping(PoolId poolId => mapping(uint16 centrifugeId => uint16)) public accountCounter; - mapping(PoolId poolId => mapping(AssetId => AccountId)) public assetIdToAccountId; + IHub public hub; + IHubRegistry public hubRegistry; + IHoldings public holdings; + IAccounting public accounting; + + mapping(PoolId => INAVHook) public navHook; + mapping(PoolId poolId => mapping(uint16 centrifugeId => bool)) public initialized; mapping(PoolId poolId => mapping(address => bool)) public manager; constructor(IHub hub_, address deployer) Auth(deployer) { @@ -52,7 +51,7 @@ contract NAVManager is INAVManager, Auth { /// @inheritdoc INAVManager function setNAVHook(PoolId poolId, INAVHook navHook_) external onlyHubManager(poolId) { - navHook = navHook_; + navHook[poolId] = navHook_; emit SetNavHook(poolId, address(navHook_)); } @@ -69,14 +68,14 @@ contract NAVManager is INAVManager, Auth { /// @inheritdoc INAVManager function initializeNetwork(PoolId poolId, uint16 centrifugeId) external onlyManager(poolId) { - require(accountCounter[poolId][centrifugeId] == 0, AlreadyInitialized()); + require(!initialized[poolId][centrifugeId], AlreadyInitialized()); hub.createAccount(poolId, equityAccount(centrifugeId), false); hub.createAccount(poolId, liabilityAccount(centrifugeId), false); hub.createAccount(poolId, gainAccount(centrifugeId), false); hub.createAccount(poolId, lossAccount(centrifugeId), false); - accountCounter[poolId][centrifugeId] = 5; + initialized[poolId][centrifugeId] = true; emit InitializeNetwork(poolId, centrifugeId); } @@ -87,15 +86,9 @@ contract NAVManager is INAVManager, Auth { onlyManager(poolId) { uint16 centrifugeId = assetId.centrifugeId(); - uint16 index = accountCounter[poolId][centrifugeId]; - require(index > 0, NotInitialized()); - require(index < type(uint16).max, ExceedsMaxAccounts()); - - AccountId assetAccount_ = assetIdToAccountId[poolId][assetId]; - if (assetAccount_.isNull()) { - assetAccount_ = withCentrifugeId(centrifugeId, index); - assetIdToAccountId[poolId][assetId] = assetAccount_; - } + require(initialized[poolId][centrifugeId], NotInitialized()); + + AccountId assetAccount_ = assetAccount(assetId); hub.createAccount(poolId, assetAccount_, true); hub.initializeHolding( @@ -109,8 +102,6 @@ contract NAVManager is INAVManager, Auth { lossAccount(centrifugeId) ); - accountCounter[poolId][centrifugeId] = index + 1; - emit InitializeHolding(poolId, scId, assetId); } @@ -120,21 +111,13 @@ contract NAVManager is INAVManager, Auth { onlyManager(poolId) { uint16 centrifugeId = assetId.centrifugeId(); - uint16 index = accountCounter[poolId][centrifugeId]; - require(index > 0, NotInitialized()); - require(index < type(uint16).max, ExceedsMaxAccounts()); - - AccountId expenseAccount_ = assetIdToAccountId[poolId][assetId]; - if (expenseAccount_.isNull()) { - expenseAccount_ = withCentrifugeId(centrifugeId, index); - assetIdToAccountId[poolId][assetId] = expenseAccount_; - } + require(initialized[poolId][centrifugeId], NotInitialized()); + + AccountId expenseAccount_ = expenseAccount(assetId); hub.createAccount(poolId, expenseAccount_, true); hub.initializeLiability(poolId, scId, assetId, valuation, expenseAccount_, liabilityAccount(centrifugeId)); - accountCounter[poolId][centrifugeId] = index + 1; - emit InitializeLiability(poolId, scId, assetId); } @@ -155,9 +138,9 @@ contract NAVManager is INAVManager, Auth { uint16 toCentrifugeId, uint128 sharesTransferred ) external auth { - require(address(navHook) != address(0), InvalidNAVHook()); + require(address(navHook[poolId]) != address(0), InvalidNAVHook()); - navHook.onTransfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); + navHook[poolId].onTransfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); } @@ -192,7 +175,7 @@ contract NAVManager is INAVManager, Auth { /// @inheritdoc INAVManager function closeGainLoss(PoolId poolId, uint16 centrifugeId) external onlyManager(poolId) { - require(accountCounter[poolId][centrifugeId] > 0, NotInitialized()); + require(initialized[poolId][centrifugeId], NotInitialized()); AccountId equityAccount_ = equityAccount(centrifugeId); AccountId gainAccount_ = gainAccount(centrifugeId); @@ -242,13 +225,15 @@ contract NAVManager is INAVManager, Auth { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVManager - function assetAccount(PoolId poolId, AssetId assetId) public view returns (AccountId) { - return assetIdToAccountId[poolId][assetId]; + function assetAccount(AssetId assetId) public pure returns (AccountId) { + // return holdings.accountId(poolId, scId, assetId, uint8(AccountType.Asset)); + return withAssetId(assetId, uint16(AccountType.Asset)); } /// @inheritdoc INAVManager - function expenseAccount(PoolId poolId, AssetId assetId) public view returns (AccountId) { - return assetAccount(poolId, assetId); + function expenseAccount(AssetId assetId) public pure returns (AccountId) { + // return holdings.accountId(poolId, scId, assetId, uint8(AccountType.Expense)); + return withAssetId(assetId, uint16(AccountType.Expense)); } /// @inheritdoc INAVManager @@ -276,10 +261,10 @@ contract NAVManager is INAVManager, Auth { //---------------------------------------------------------------------------------------------- function _onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) internal { - require(address(navHook) != address(0), InvalidNAVHook()); + require(address(navHook[poolId]) != address(0), InvalidNAVHook()); uint128 netAssetValue_ = netAssetValue(poolId, centrifugeId); - navHook.onUpdate(poolId, scId, centrifugeId, netAssetValue_); + navHook[poolId].onUpdate(poolId, scId, centrifugeId, netAssetValue_); emit Sync(poolId, scId, centrifugeId, netAssetValue_); } diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 83b08a7f5..633ff4e12 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -23,10 +23,8 @@ contract SimplePriceManager is ISimplePriceManager, Auth { IHubRegistry public immutable hubRegistry; IShareClassManager public immutable shareClassManager; - mapping(PoolId poolId => uint16[]) public networks; - mapping(PoolId poolId => uint128) public globalIssuance; - mapping(PoolId poolId => uint128) public globalNetAssetValue; - mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public metrics; + mapping(PoolId poolId => Metrics) public metrics; + mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; mapping(PoolId poolId => mapping(address => bool)) public manager; constructor(IHub hub_, address deployer) Auth(deployer) { @@ -53,9 +51,14 @@ contract SimplePriceManager is ISimplePriceManager, Auth { // Administration //---------------------------------------------------------------------------------------------- + /// @inheritdoc ISimplePriceManager + function networks(PoolId poolId) external view returns (uint16[] memory) { + return metrics[poolId].networks; + } + /// @inheritdoc ISimplePriceManager function setNetworks(PoolId poolId, uint16[] calldata centrifugeIds) external onlyHubManager(poolId) { - networks[poolId] = centrifugeIds; + metrics[poolId].networks = centrifugeIds; } /// @inheritdoc ISimplePriceManager @@ -71,28 +74,28 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc INAVHook function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { - NetworkMetrics storage networkMetrics = metrics[poolId][centrifugeId]; + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - globalIssuance[poolId] = globalIssuance[poolId] + issuance - networkMetrics.issuance; - globalNetAssetValue[poolId] = globalNetAssetValue[poolId] + netAssetValue - networkMetrics.netAssetValue; + metrics[poolId].issuance = metrics[poolId].issuance + issuance - networkMetrics_.issuance; + metrics[poolId].netAssetValue = metrics[poolId].netAssetValue + netAssetValue - networkMetrics_.netAssetValue; D18 price = _navPerShare(poolId); - networkMetrics.netAssetValue = netAssetValue; - networkMetrics.issuance = issuance; + networkMetrics_.netAssetValue = netAssetValue; + networkMetrics_.issuance = issuance; - uint256 networkCount = networks[poolId].length; + uint256 networkCount = metrics[poolId].networks.length; bytes[] memory cs = new bytes[](networkCount + 1); cs[0] = abi.encodeWithSelector(hub.updateSharePrice.selector, poolId, scId, price); for (uint256 i; i < networkCount; i++) { - cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, networks[poolId][i]); + cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, metrics[poolId].networks[i]); } IMulticall(address(hub)).multicall{value: MAX_MESSAGE_COST * (cs.length)}(cs); - emit Update(poolId, globalNetAssetValue[poolId], globalIssuance[poolId], price); + emit Update(poolId, metrics[poolId].netAssetValue, metrics[poolId].issuance, price); } /// @inheritdoc INAVHook @@ -103,8 +106,8 @@ contract SimplePriceManager is ISimplePriceManager, Auth { uint16 toCentrifugeId, uint128 sharesTransferred ) external auth { - NetworkMetrics storage fromMetrics = metrics[poolId][fromCentrifugeId]; - NetworkMetrics storage toMetrics = metrics[poolId][toCentrifugeId]; + NetworkMetrics storage fromMetrics = networkMetrics[poolId][fromCentrifugeId]; + NetworkMetrics storage toMetrics = networkMetrics[poolId][toCentrifugeId]; fromMetrics.issuance -= sharesTransferred; toMetrics.issuance += sharesTransferred; @@ -156,7 +159,9 @@ contract SimplePriceManager is ISimplePriceManager, Auth { //---------------------------------------------------------------------------------------------- function _navPerShare(PoolId poolId) internal view returns (D18) { - return globalIssuance[poolId] == 0 ? d18(1, 1) : d18(globalNetAssetValue[poolId]) / d18(globalIssuance[poolId]); + return metrics[poolId].issuance == 0 + ? d18(1, 1) + : d18(metrics[poolId].netAssetValue) / d18(metrics[poolId].issuance); } // TODO: remove when not needed anymore diff --git a/src/managers/hub/interfaces/INAVManager.sol b/src/managers/hub/interfaces/INAVManager.sol index 145050234..bc36cf1cc 100644 --- a/src/managers/hub/interfaces/INAVManager.sol +++ b/src/managers/hub/interfaces/INAVManager.sol @@ -57,8 +57,14 @@ interface INAVManager is ISnapshotHook { // Administration //---------------------------------------------------------------------------------------------- + /// @notice Check if a network has been initialized for a pool + /// @param poolId The pool ID + /// @param centrifugeId The Centrifuge ID of the network + function initialized(PoolId poolId, uint16 centrifugeId) external view returns (bool); + /// @notice Get the NAV hook - function navHook() external view returns (INAVHook); + /// @param poolId The pool ID + function navHook(PoolId poolId) external view returns (INAVHook); /// @notice Set the NAV hook contract that will receive NAV updates /// @param poolId The pool ID @@ -145,14 +151,12 @@ interface INAVManager is ISnapshotHook { //---------------------------------------------------------------------------------------------- /// @notice Get the asset account ID for a specific asset on a network - /// @param poolId The pool ID /// @param assetId The asset ID - function assetAccount(PoolId poolId, AssetId assetId) external view returns (AccountId); + function assetAccount(AssetId assetId) external view returns (AccountId); /// @notice Get the expense account ID for a specific asset on a network - /// @param poolId The pool ID /// @param assetId The asset ID - function expenseAccount(PoolId poolId, AssetId assetId) external view returns (AccountId); + function expenseAccount(AssetId assetId) external view returns (AccountId); /// @notice Get the equity account ID for a specific network /// @param centrifugeId The Centrifuge ID of the network diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index 0b159268b..46f3c774c 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -19,18 +19,23 @@ interface ISimplePriceManager is INAVHook { error InvalidShareClassCount(); error MismatchedEpochs(); + struct Metrics { + uint128 netAssetValue; + uint128 issuance; + uint16[] networks; + } + struct NetworkMetrics { uint128 netAssetValue; uint128 issuance; } - function globalIssuance(PoolId poolId) external view returns (uint128); - function globalNetAssetValue(PoolId poolId) external view returns (uint128); - function metrics(PoolId poolId, uint16 centrifugeId) + function metrics(PoolId poolId) external view returns (uint128 netAssetValue, uint128 issuance); + function networks(PoolId poolId) external view returns (uint16[] memory networks); + function networkMetrics(PoolId poolId, uint16 centrifugeId) external view returns (uint128 netAssetValue, uint128 issuance); - function networks(PoolId poolId, uint256 index) external view returns (uint16); function manager(PoolId poolId, address manager_) external view returns (bool); //---------------------------------------------------------------------------------------------- diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index abbed1639..4e33bb2c4 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -115,10 +115,9 @@ contract NAVManagerIntegrationTest is BaseTest { uint128 navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); uint128 navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (uint128 navHub2, uint128 issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); - (uint128 navSpoke2, uint128 issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); - uint128 globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); - uint128 globalIssuance = simplePriceManager.globalIssuance(POOL_A); + (uint128 navHub2, uint128 issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (uint128 navSpoke2, uint128 issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (uint128 globalNAV, uint128 globalIssuance) = simplePriceManager.metrics(POOL_A); assertEq(navHub, 500e18); assertEq(navSpoke, 3300e18); @@ -144,10 +143,9 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); - globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); - globalIssuance = simplePriceManager.globalIssuance(POOL_A); + (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); (bool spokeGainIsPositive, uint128 spokeGain) = accounting.accountValue(POOL_A, navManager.gainAccount(CHAIN_CV)); (bool hubLossIsPositive, uint128 hubLoss) = accounting.accountValue(POOL_A, navManager.lossAccount(CHAIN_CP)); @@ -171,10 +169,9 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); - globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); - globalIssuance = simplePriceManager.globalIssuance(POOL_A); + (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); // NAV and global issuance should remain unchanged, only issuance per network changes assertEq(navHub, 250e18, "navHub3"); @@ -196,10 +193,9 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); - globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); - globalIssuance = simplePriceManager.globalIssuance(POOL_A); + (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); // Liability reduces the NAV assertEq(navHub, 200e18); @@ -221,10 +217,9 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.metrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.metrics(POOL_A, CHAIN_CV); - globalNAV = simplePriceManager.globalNetAssetValue(POOL_A); - globalIssuance = simplePriceManager.globalIssuance(POOL_A); + (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); // NAV should remain unchanged assertEq(navHub, 200e18); diff --git a/test/managers/hub/unit/NAVManager.t.sol b/test/managers/hub/unit/NAVManager.t.sol index 7faebde01..2d59b99fc 100644 --- a/test/managers/hub/unit/NAVManager.t.sol +++ b/test/managers/hub/unit/NAVManager.t.sol @@ -10,9 +10,9 @@ import {MockValuation} from "../../../common/mocks/MockValuation.sol"; import {PoolId} from "../../../../src/common/types/PoolId.sol"; import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; -import {AccountId, withCentrifugeId} from "../../../../src/common/types/AccountId.sol"; +import {AccountId, withCentrifugeId, withAssetId} from "../../../../src/common/types/AccountId.sol"; -import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; +import {IHub, AccountType} from "../../../../src/hub/interfaces/IHub.sol"; import {IHoldings} from "../../../../src/hub/interfaces/IHoldings.sol"; import {IAccounting} from "../../../../src/hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; @@ -110,7 +110,6 @@ contract NAVManagerConstructorTest is NAVManagerTest { assertEq(address(navManager.hub()), address(hub)); assertEq(address(navManager.holdings()), holdings); assertEq(address(navManager.accounting()), address(accounting)); - assertEq(address(navManager.navHook()), address(0)); } } @@ -122,7 +121,8 @@ contract NAVManagerConfigureTest is NAVManagerTest { vm.prank(hubManager); navManager.setNAVHook(POOL_A, navHook); - assertEq(address(navManager.navHook()), address(navHook)); + assertEq(address(navManager.navHook(POOL_A)), address(navHook)); + assertEq(address(navManager.navHook(POOL_B)), address(0)); } function testSetNAVHookUnauthorized() public { @@ -135,7 +135,7 @@ contract NAVManagerConfigureTest is NAVManagerTest { vm.prank(hubManager); navManager.setNAVHook(POOL_A, INAVHook(address(0))); - assertEq(address(navManager.navHook()), address(0)); + assertEq(address(navManager.navHook(POOL_A)), address(0)); } function testUpdateManagerSuccess() public { @@ -202,7 +202,7 @@ contract NAVManagerConfigureTest is NAVManagerTest { vm.prank(manager); navManager.initializeNetwork(POOL_A, CENTRIFUGE_ID_1); - assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 5); + assertTrue(navManager.initialized(POOL_A, CENTRIFUGE_ID_1)); } function testInitializeNetworkAlreadyInitialized() public { @@ -229,7 +229,7 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { } function testInitializeHoldingSuccess() public { - AccountId expectedAssetAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); + AccountId expectedAssetAccount = withAssetId(asset1, uint16(AccountType.Asset)); vm.expectCall( address(hub), abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, expectedAssetAccount, true) @@ -255,8 +255,7 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { vm.prank(manager); navManager.initializeHolding(POOL_A, SC_1, asset1, mockValuation); - assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 6); - assertEq(navManager.assetAccount(POOL_A, asset1).raw(), expectedAssetAccount.raw()); + assertEq(navManager.assetAccount(asset1).raw(), expectedAssetAccount.raw()); } function testInitializeHoldingNotInitialized() public { @@ -269,7 +268,7 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { vm.prank(manager); navManager.initializeHolding(POOL_A, SC_1, asset1, mockValuation); - AccountId expectedAssetAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); + AccountId expectedAssetAccount = withAssetId(asset1, uint16(AccountType.Asset)); vm.expectCall( address(hub), abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, expectedAssetAccount, true) @@ -278,8 +277,7 @@ contract NAVManagerHoldingInitializationTest is NAVManagerTest { vm.prank(manager); navManager.initializeHolding(POOL_A, SC_2, asset1, mockValuation); - // Account counter should increment again - assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 7); + assertEq(navManager.assetAccount(asset1).raw(), expectedAssetAccount.raw()); } function testInitializeHoldingUnauthorized() public { @@ -297,7 +295,7 @@ contract NAVManagerLiabilityInitializationTest is NAVManagerTest { } function testInitializeLiabilitySuccess() public { - AccountId expectedExpenseAccount = withCentrifugeId(CENTRIFUGE_ID_1, 5); + AccountId expectedExpenseAccount = withAssetId(asset1, uint16(AccountType.Expense)); vm.expectCall( address(hub), abi.encodeWithSelector(IHub.createAccount.selector, POOL_A, expectedExpenseAccount, true) @@ -321,8 +319,7 @@ contract NAVManagerLiabilityInitializationTest is NAVManagerTest { vm.prank(manager); navManager.initializeLiability(POOL_A, SC_1, asset1, mockValuation); - assertEq(navManager.accountCounter(POOL_A, CENTRIFUGE_ID_1), 6); - assertEq(navManager.expenseAccount(POOL_A, asset1).raw(), expectedExpenseAccount.raw()); + assertEq(navManager.expenseAccount(asset1).raw(), expectedExpenseAccount.raw()); } function testInitializeLiabilityNotInitialized() public { @@ -640,8 +637,8 @@ contract NAVManagerHelperFunctionsTest is NAVManagerTest { vm.prank(manager); navManager.initializeHolding(POOL_A, SC_1, asset1, mockValuation); - AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); - AccountId actual = navManager.assetAccount(POOL_A, asset1); + AccountId expected = withAssetId(asset1, uint16(AccountType.Asset)); + AccountId actual = navManager.assetAccount(asset1); assertEq(actual.raw(), expected.raw()); } @@ -651,15 +648,10 @@ contract NAVManagerHelperFunctionsTest is NAVManagerTest { vm.prank(manager); navManager.initializeLiability(POOL_A, SC_1, asset1, mockValuation); - AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 5); - AccountId actual = navManager.expenseAccount(POOL_A, asset1); + AccountId expected = withAssetId(asset1, uint16(AccountType.Expense)); + AccountId actual = navManager.expenseAccount(asset1); assertEq(actual.raw(), expected.raw()); } - - function testAssetAccountNotInitialized() public view { - AccountId actual = navManager.assetAccount(POOL_A, asset1); - assertTrue(actual.isNull()); - } } contract NAVManagerOnTransferTest is NAVManagerTest { diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index 6116f6cee..7a37c8438 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -125,10 +125,12 @@ contract SimplePriceManagerTest is Test { contract SimplePriceManagerConstructorTest is SimplePriceManagerTest { function testConstructorSuccess() public view { + (uint128 globalNAV, uint128 globalIssuance) = priceManager.metrics(POOL_A); + assertEq(address(priceManager.hub()), hub); assertEq(address(priceManager.shareClassManager()), shareClassManager); - assertEq(priceManager.globalIssuance(POOL_A), 0); - assertEq(priceManager.globalNetAssetValue(POOL_A), 0); + assertEq(globalNAV, 0); + assertEq(globalIssuance, 0); } } @@ -142,9 +144,11 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { vm.prank(hubManager); priceManager.setNetworks(POOL_A, networks); - assertEq(priceManager.networks(POOL_A, 0), CENTRIFUGE_ID_1); - assertEq(priceManager.networks(POOL_A, 1), CENTRIFUGE_ID_2); - assertEq(priceManager.networks(POOL_A, 2), CENTRIFUGE_ID_3); + uint16[] memory storedNetworks = priceManager.networks(POOL_A); + + assertEq(storedNetworks[0], CENTRIFUGE_ID_1); + assertEq(storedNetworks[1], CENTRIFUGE_ID_2); + assertEq(storedNetworks[2], CENTRIFUGE_ID_3); } function testSetNetworksUnauthorized() public { @@ -232,12 +236,13 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, netAssetValue); - assertEq(priceManager.globalIssuance(POOL_A), 100); - assertEq(priceManager.globalNetAssetValue(POOL_A), netAssetValue); + (uint128 globalNAV, uint128 globalIssuance) = priceManager.metrics(POOL_A); + assertEq(globalIssuance, 100); + assertEq(globalNAV, netAssetValue); - (uint128 storedNAV, uint128 storedIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(storedNAV, netAssetValue); - assertEq(storedIssuance, 100); + (uint128 networkNAV, uint128 networkIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(networkNAV, netAssetValue); + assertEq(networkIssuance, 100); } function testOnUpdateSecondNetwork() public { @@ -255,8 +260,9 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, netAssetValue2); - assertEq(priceManager.globalIssuance(POOL_A), 300); // 100 + 200 - assertEq(priceManager.globalNetAssetValue(POOL_A), 2700); // 1000 + 1700 + (uint128 globalNAV, uint128 globalIssuance) = priceManager.metrics(POOL_A); + assertEq(globalIssuance, 300); // 100 + 200 + assertEq(globalNAV, 2700); // 1000 + 1700 } function testOnUpdateExistingNetwork() public { @@ -277,8 +283,9 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, newNetAssetValue); - assertEq(priceManager.globalIssuance(POOL_A), 150); - assertEq(priceManager.globalNetAssetValue(POOL_A), 1200); + (uint128 globalNAV, uint128 globalIssuance) = priceManager.metrics(POOL_A); + assertEq(globalIssuance, 150); + assertEq(globalNAV, 1200); } function testOnUpdateUnauthorized() public { @@ -299,8 +306,9 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); - assertEq(priceManager.globalIssuance(POOL_A), 0); - assertEq(priceManager.globalNetAssetValue(POOL_A), 1000); + (uint128 globalNAV, uint128 globalIssuance) = priceManager.metrics(POOL_A); + assertEq(globalIssuance, 0); + assertEq(globalNAV, 1000); } } @@ -324,8 +332,8 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); - (uint128 fromNAV, uint128 fromIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_1); - (uint128 toNAV, uint128 toIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_2); + (uint128 fromNAV, uint128 fromIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + (uint128 toNAV, uint128 toIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); assertEq(fromIssuance, 50); // 100 - 50 assertEq(toIssuance, 250); // 200 + 50 @@ -345,8 +353,8 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 0); - (, uint128 fromIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_1); - (, uint128 toIssuance) = priceManager.metrics(POOL_A, CENTRIFUGE_ID_2); + (, uint128 fromIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + (, uint128 toIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); assertEq(fromIssuance, 100); assertEq(toIssuance, 200); From 18e62d150d13b300c08e7a71d678fba08b134728 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:04:43 +0200 Subject: [PATCH 56/83] remove comment --- src/managers/hub/NAVManager.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index bbd79be44..411d36139 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -226,13 +226,11 @@ contract NAVManager is INAVManager, Auth { /// @inheritdoc INAVManager function assetAccount(AssetId assetId) public pure returns (AccountId) { - // return holdings.accountId(poolId, scId, assetId, uint8(AccountType.Asset)); return withAssetId(assetId, uint16(AccountType.Asset)); } /// @inheritdoc INAVManager function expenseAccount(AssetId assetId) public pure returns (AccountId) { - // return holdings.accountId(poolId, scId, assetId, uint8(AccountType.Expense)); return withAssetId(assetId, uint16(AccountType.Expense)); } From 732103bdacf093cd878b7ca99801952350beceac Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:35:59 +0200 Subject: [PATCH 57/83] gateway methods --- script/HubManagersDeployer.s.sol | 9 ++++--- src/managers/hub/SimplePriceManager.sol | 11 ++++---- .../managers/hub/integration/NAVManager.t.sol | 2 +- .../hub/unit/SimplePriceManager.t.sol | 27 ++++++++++++++----- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/script/HubManagersDeployer.s.sol b/script/HubManagersDeployer.s.sol index a747e2d92..70c8e96c9 100644 --- a/script/HubManagersDeployer.s.sol +++ b/script/HubManagersDeployer.s.sol @@ -17,6 +17,10 @@ struct HubManagersReport { contract HubManagersActionBatcher is HubActionBatcher { function engageManagers(HubManagersReport memory report) public onlyDeployer { + // Rely root + report.navManager.rely(address(report.hub.common.root)); + report.simplePriceManager.rely(address(report.hub.common.root)); + // Rely hub report.navManager.rely(address(report.hub.hub)); report.simplePriceManager.rely(address(report.hub.hub)); @@ -24,10 +28,7 @@ contract HubManagersActionBatcher is HubActionBatcher { // Rely other report.simplePriceManager.rely(address(report.navManager)); report.navManager.rely(address(report.hub.holdings)); - - // Rely root - report.navManager.rely(address(report.hub.common.root)); - report.simplePriceManager.rely(address(report.hub.common.root)); + report.hub.common.gateway.rely(address(report.simplePriceManager)); } function revokeManagers(HubManagersReport memory report) public onlyDeployer { diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 633ff4e12..122919713 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -12,6 +12,7 @@ import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {MAX_MESSAGE_COST} from "../../common/interfaces/IGasService.sol"; +import {IGateway} from "../../common/interfaces/IGateway.sol"; import {IHub} from "../../hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; @@ -20,6 +21,7 @@ import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; /// @notice Share price calculation manager for single share class pools. contract SimplePriceManager is ISimplePriceManager, Auth { IHub public immutable hub; + IGateway public gateway; IHubRegistry public immutable hubRegistry; IShareClassManager public immutable shareClassManager; @@ -29,6 +31,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { constructor(IHub hub_, address deployer) Auth(deployer) { hub = hub_; + gateway = hub_.gateway(); hubRegistry = hub_.hubRegistry(); shareClassManager = hub_.shareClassManager(); @@ -86,15 +89,13 @@ contract SimplePriceManager is ISimplePriceManager, Auth { networkMetrics_.issuance = issuance; uint256 networkCount = metrics[poolId].networks.length; - bytes[] memory cs = new bytes[](networkCount + 1); - cs[0] = abi.encodeWithSelector(hub.updateSharePrice.selector, poolId, scId, price); + gateway.startBatching(); + hub.updateSharePrice(poolId, scId, price); for (uint256 i; i < networkCount; i++) { - cs[i + 1] = abi.encodeWithSelector(hub.notifySharePrice.selector, poolId, scId, metrics[poolId].networks[i]); + hub.notifySharePrice(poolId, scId, metrics[poolId].networks[i]); } - IMulticall(address(hub)).multicall{value: MAX_MESSAGE_COST * (cs.length)}(cs); - emit Update(poolId, metrics[poolId].netAssetValue, metrics[poolId].issuance, price); } diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index 4e33bb2c4..9e6b8f67c 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -47,7 +47,7 @@ contract NAVManagerIntegrationTest is BaseTest { } function _setupMocks() internal { - vm.mockCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector), abi.encode()); + vm.mockCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector), abi.encode(uint256(0))); } function _setupPool() internal { diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index 7a37c8438..cc656b3f4 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -9,6 +9,7 @@ import {PoolId} from "../../../../src/common/types/PoolId.sol"; import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; +import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; @@ -37,6 +38,7 @@ contract SimplePriceManagerTest is Test { AssetId asset2 = newAssetId(2, 1); address hub = address(new MockHub()); + address gateway = address(new IsContract()); address hubRegistry = address(new IsContract()); address shareClassManager = address(new IsContract()); @@ -44,6 +46,7 @@ contract SimplePriceManagerTest is Test { address hubManager = makeAddr("hubManager"); address manager = makeAddr("manager"); address caller = makeAddr("caller"); + address auth = makeAddr("auth"); SimplePriceManager priceManager; @@ -55,15 +58,26 @@ contract SimplePriceManagerTest is Test { function _setupMocks() internal { vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.gateway.selector), abi.encode(gateway)); + + vm.mockCall(gateway, abi.encodeWithSelector(IGateway.startBatching.selector), abi.encode()); + vm.mockCall(gateway, abi.encodeWithSelector(IGateway.endBatching.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); - vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode()); - vm.mockCall(hub, abi.encodeWithSelector(IHub.approveDeposits.selector), abi.encode(uint128(0), uint128(0))); + vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); + vm.mockCall( + hub, abi.encodeWithSelector(IHub.approveDeposits.selector), abi.encode(uint256(0), uint128(0), uint128(0)) + ); vm.mockCall( - hub, abi.encodeWithSelector(IHub.issueShares.selector), abi.encode(uint128(0), uint128(0), uint128(0)) + hub, + abi.encodeWithSelector(IHub.issueShares.selector), + abi.encode(uint256(0), uint128(0), uint128(0), uint128(0)) ); - vm.mockCall(hub, abi.encodeWithSelector(IHub.approveRedeems.selector), abi.encode(uint128(0))); + vm.mockCall(hub, abi.encodeWithSelector(IHub.approveRedeems.selector), abi.encode(uint256(0), uint128(0))); vm.mockCall( - hub, abi.encodeWithSelector(IHub.revokeShares.selector), abi.encode(uint128(0), uint128(0), uint128(0)) + hub, + abi.encodeWithSelector(IHub.revokeShares.selector), + abi.encode(uint256(0), uint128(0), uint128(0), uint128(0)) ); vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); vm.mockCall( @@ -113,7 +127,8 @@ contract SimplePriceManagerTest is Test { } function _deployManager() internal { - priceManager = new SimplePriceManager(IHub(hub), address(this)); + priceManager = new SimplePriceManager(IHub(hub), auth); + vm.prank(auth); priceManager.rely(caller); vm.prank(hubManager); From 1988633f195ceebdc136949a5c3bd6e1d220bc67 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:36:17 +0200 Subject: [PATCH 58/83] file --- src/managers/hub/SimplePriceManager.sol | 7 +++++ .../hub/interfaces/ISimplePriceManager.sol | 4 +++ .../hub/unit/SimplePriceManager.t.sol | 30 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 122919713..c9091b147 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -54,6 +54,13 @@ contract SimplePriceManager is ISimplePriceManager, Auth { // Administration //---------------------------------------------------------------------------------------------- + /// @inheritdoc ISimplePriceManager + function file(bytes32 what, address data) external auth { + if (what == "gateway") gateway = IGateway(data); + else revert ISimplePriceManager.FileUnrecognizedParam(); + emit File(what, data); + } + /// @inheritdoc ISimplePriceManager function networks(PoolId poolId) external view returns (uint16[] memory) { return metrics[poolId].networks; diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index 46f3c774c..c639f0787 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -15,9 +15,11 @@ interface ISimplePriceManager is INAVHook { PoolId indexed poolId, uint16 indexed fromCentrifugeId, uint16 indexed toCentrifugeId, uint128 sharesTransferred ); event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); + event File(bytes32 indexed what, address data); error InvalidShareClassCount(); error MismatchedEpochs(); + error FileUnrecognizedParam(); struct Metrics { uint128 netAssetValue; @@ -54,6 +56,8 @@ interface ISimplePriceManager is INAVHook { /// @param canManage Whether the address can manage this manager function updateManager(PoolId poolId, address manager, bool canManage) external; + function file(bytes32 what, address data) external; + //---------------------------------------------------------------------------------------------- // Manager actions //---------------------------------------------------------------------------------------------- diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index cc656b3f4..c5fb2e16f 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -219,6 +219,36 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { } } +contract SimplePriceManagerFileTests is SimplePriceManagerTest { + function testFileGateway() public { + address newGateway = makeAddr("newGateway"); + + vm.expectEmit(true, false, true, true); + emit ISimplePriceManager.File("gateway", newGateway); + + vm.prank(auth); + priceManager.file("gateway", newGateway); + + assertEq(address(priceManager.gateway()), newGateway); + } + + function testFileUnrecognizedParam() public { + address someAddress = makeAddr("someAddress"); + + vm.expectRevert(ISimplePriceManager.FileUnrecognizedParam.selector); + vm.prank(auth); + priceManager.file("invalid", someAddress); + } + + function testFileUnauthorized() public { + address newGateway = makeAddr("newGateway"); + + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.file("gateway", newGateway); + } +} + contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { function setUp() public override { super.setUp(); From 0678fc16f6c7038423cebb1aa10ae84bbd565a3d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:37:13 +0200 Subject: [PATCH 59/83] immutable --- src/managers/hub/NAVManager.sol | 8 ++++---- src/managers/hub/SimplePriceManager.sol | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index 411d36139..a9dfba473 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -19,10 +19,10 @@ import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is INAVManager, Auth { - IHub public hub; - IHubRegistry public hubRegistry; - IHoldings public holdings; - IAccounting public accounting; + IHub public immutable hub; + IHubRegistry public immutable hubRegistry; + IHoldings public immutable holdings; + IAccounting public immutable accounting; mapping(PoolId => INAVHook) public navHook; mapping(PoolId poolId => mapping(uint16 centrifugeId => bool)) public initialized; diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index c9091b147..9b47f0c0d 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -20,8 +20,8 @@ import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; /// @notice Share price calculation manager for single share class pools. contract SimplePriceManager is ISimplePriceManager, Auth { - IHub public immutable hub; IGateway public gateway; + IHub public immutable hub; IHubRegistry public immutable hubRegistry; IShareClassManager public immutable shareClassManager; From 62728ff9931c30eceaf52bb9caae502eef5f22b4 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:43:09 +0200 Subject: [PATCH 60/83] gas savings --- src/managers/hub/SimplePriceManager.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 9b47f0c0d..76de93f77 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -85,25 +85,26 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc INAVHook function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; + Metrics storage metrics_ = metrics[poolId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - metrics[poolId].issuance = metrics[poolId].issuance + issuance - networkMetrics_.issuance; - metrics[poolId].netAssetValue = metrics[poolId].netAssetValue + netAssetValue - networkMetrics_.netAssetValue; + metrics_.issuance = metrics_.issuance + issuance - networkMetrics_.issuance; + metrics_.netAssetValue = metrics_.netAssetValue + netAssetValue - networkMetrics_.netAssetValue; D18 price = _navPerShare(poolId); networkMetrics_.netAssetValue = netAssetValue; networkMetrics_.issuance = issuance; - uint256 networkCount = metrics[poolId].networks.length; + uint256 networkCount = metrics_.networks.length; gateway.startBatching(); hub.updateSharePrice(poolId, scId, price); for (uint256 i; i < networkCount; i++) { - hub.notifySharePrice(poolId, scId, metrics[poolId].networks[i]); + hub.notifySharePrice(poolId, scId, metrics_.networks[i]); } - emit Update(poolId, metrics[poolId].netAssetValue, metrics[poolId].issuance, price); + emit Update(poolId, metrics_.netAssetValue, metrics_.issuance, price); } /// @inheritdoc INAVHook From 450b859ddad0ef8c34c54edcf5f286030a8cde8d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:55:56 +0200 Subject: [PATCH 61/83] batchrequestmanasger changes --- src/hub/interfaces/IHub.sol | 2 + src/managers/hub/SimplePriceManager.sol | 27 +++-- test/hub/integration/BaseTest.sol | 3 +- .../hub/unit/SimplePriceManager.t.sol | 113 ++++++++++++------ 4 files changed, 97 insertions(+), 48 deletions(-) diff --git a/src/hub/interfaces/IHub.sol b/src/hub/interfaces/IHub.sol index ec9c7f6ee..12599da05 100644 --- a/src/hub/interfaces/IHub.sol +++ b/src/hub/interfaces/IHub.sol @@ -6,6 +6,7 @@ import {IHubRegistry} from "./IHubRegistry.sol"; import {IAccounting, JournalEntry} from "./IAccounting.sol"; import {IHubRequestManager} from "./IHubRequestManager.sol"; import {IShareClassManager} from "./IShareClassManager.sol"; +import {IHubHelpers} from "./IHubHelpers.sol"; import {D18} from "../../misc/types/D18.sol"; @@ -100,6 +101,7 @@ interface IHub { function holdings() external view returns (IHoldings); function accounting() external view returns (IAccounting); function hubRegistry() external view returns (IHubRegistry); + function hubHelpers() external view returns (IHubHelpers); function sender() external view returns (IHubMessageSender); function shareClassManager() external view returns (IShareClassManager); diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 76de93f77..4a1bb1d8a 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -17,6 +17,7 @@ import {IGateway} from "../../common/interfaces/IGateway.sol"; import {IHub} from "../../hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; +import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; /// @notice Share price calculation manager for single share class pools. contract SimplePriceManager is ISimplePriceManager, Auth { @@ -135,14 +136,19 @@ contract SimplePriceManager is ISimplePriceManager, Auth { uint128 approvedAssetAmount, uint128 extraGasLimit ) external onlyManager(poolId) { - uint32 nowDepositEpochId = shareClassManager.nowDepositEpoch(scId, depositAssetId); - uint32 nowIssueEpochId = shareClassManager.nowIssueEpoch(scId, depositAssetId); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); + uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); + D18 pricePoolPerAsset = hub.hubHelpers().pricePoolPerAsset(poolId, scId, depositAssetId); D18 navPoolPerShare = _navPerShare(poolId); - hub.approveDeposits(poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount); - hub.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + requestManager.approveDeposits( + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset + ); + requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); } /// @inheritdoc ISimplePriceManager @@ -153,14 +159,19 @@ contract SimplePriceManager is ISimplePriceManager, Auth { uint128 approvedShareAmount, uint128 extraGasLimit ) external onlyManager(poolId) { - uint32 nowRedeemEpochId = shareClassManager.nowRedeemEpoch(scId, payoutAssetId); - uint32 nowRevokeEpochId = shareClassManager.nowRevokeEpoch(scId, payoutAssetId); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); + uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); + D18 pricePoolPerAsset = hub.hubHelpers().pricePoolPerAsset(poolId, scId, payoutAssetId); D18 navPoolPerShare = _navPerShare(poolId); - hub.approveRedeems(poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount); - hub.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + requestManager.approveRedeems( + poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset + ); + requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); } //---------------------------------------------------------------------------------------------- diff --git a/test/hub/integration/BaseTest.sol b/test/hub/integration/BaseTest.sol index 78f94dcd1..357d88620 100644 --- a/test/hub/integration/BaseTest.sol +++ b/test/hub/integration/BaseTest.sol @@ -88,9 +88,8 @@ contract BaseTest is ExtendedHubDeployer, Test { labelAddresses(""); deployExtendedHub(input, batcher); _mockStuff(batcher); - removeExtendedHubDeployerAccess(batcher); hubRequestManager = IHubRequestManager(address(new MockHubRequestManager(address(hub)))); - removeHubDeployerAccess(batcher); + removeExtendedHubDeployerAccess(batcher); // Initialize accounts vm.deal(FM, 1 ether); diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index c5fb2e16f..bb10d0e64 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -10,8 +10,10 @@ import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; +import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; +import {IHubHelpers} from "../../../../src/hub/interfaces/IHubHelpers.sol"; import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; @@ -41,6 +43,8 @@ contract SimplePriceManagerTest is Test { address gateway = address(new IsContract()); address hubRegistry = address(new IsContract()); address shareClassManager = address(new IsContract()); + address batchRequestManager = address(new IsContract()); + address hubHelpers = address(new IsContract()); address unauthorized = makeAddr("unauthorized"); address hubManager = makeAddr("hubManager"); @@ -59,25 +63,17 @@ contract SimplePriceManagerTest is Test { vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); vm.mockCall(hub, abi.encodeWithSelector(IHub.gateway.selector), abi.encode(gateway)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.hubHelpers.selector), abi.encode(hubHelpers)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); vm.mockCall(gateway, abi.encodeWithSelector(IGateway.startBatching.selector), abi.encode()); vm.mockCall(gateway, abi.encodeWithSelector(IGateway.endBatching.selector), abi.encode()); - vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); - vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); vm.mockCall( - hub, abi.encodeWithSelector(IHub.approveDeposits.selector), abi.encode(uint256(0), uint128(0), uint128(0)) - ); - vm.mockCall( - hub, - abi.encodeWithSelector(IHub.issueShares.selector), - abi.encode(uint256(0), uint128(0), uint128(0), uint128(0)) - ); - vm.mockCall(hub, abi.encodeWithSelector(IHub.approveRedeems.selector), abi.encode(uint256(0), uint128(0))); - vm.mockCall( - hub, - abi.encodeWithSelector(IHub.revokeShares.selector), - abi.encode(uint256(0), uint128(0), uint128(0), uint128(0)) + hubRegistry, + abi.encodeWithSelector(IHubRegistry.hubRequestManager.selector), + abi.encode(batchRequestManager) ); vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); vm.mockCall( @@ -104,26 +100,51 @@ contract SimplePriceManagerTest is Test { abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_2), abi.encode(200) ); + vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowDepositEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), abi.encode(1) ); vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowIssueEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), abi.encode(1) ); vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowRedeemEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), abi.encode(2) ); vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowRevokeEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), abi.encode(2) ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.approveDeposits.selector), + abi.encode(uint256(0)) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.issueShares.selector), + abi.encode(uint256(0)) + ); + vm.mockCall( + batchRequestManager, abi.encodeWithSelector(IBatchRequestManager.approveRedeems.selector), abi.encode() + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.revokeShares.selector), + abi.encode(uint256(0)) + ); + + vm.mockCall( + hubHelpers, + abi.encodeWithSelector(IHubHelpers.pricePoolPerAsset.selector, POOL_A, SC_1, asset1), + abi.encode(d18(1, 1)) + ); } function _deployManager() internal { @@ -420,13 +441,21 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 vm.expectCall( - address(hub), - abi.encodeWithSelector(IHub.approveDeposits.selector, POOL_A, SC_1, asset1, 1, approvedAssetAmount) + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveDeposits.selector, POOL_A, SC_1, asset1, 1, approvedAssetAmount, d18(1, 1) + ) ); vm.expectCall( - address(hub), + address(batchRequestManager), abi.encodeWithSelector( - IHub.issueShares.selector, POOL_A, SC_1, asset1, uint32(1), expectedNavPerShare, extraGasLimit + IBatchRequestManager.issueShares.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + expectedNavPerShare, + extraGasLimit ) ); @@ -442,13 +471,13 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { function testApproveDepositsAndIssueSharesMismatchedEpochs() public { vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowDepositEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), abi.encode(1) ); vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowIssueEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), abi.encode(2) ); @@ -462,13 +491,21 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { uint128 extraGasLimit = 100000; vm.expectCall( - address(hub), - abi.encodeWithSelector(IHub.approveRedeems.selector, POOL_A, SC_1, asset1, uint32(2), approvedShareAmount) + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveRedeems.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + approvedShareAmount, + d18(1, 1) + ) ); vm.expectCall( - address(hub), + address(batchRequestManager), abi.encodeWithSelector( - IHub.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit + IBatchRequestManager.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit ) // 1000/100 = 10 ); @@ -484,13 +521,13 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { function testApproveRedeemsAndRevokeSharesMismatchedEpochs() public { vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowRedeemEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), abi.encode(2) ); vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.nowRevokeEpoch.selector, SC_1, asset1), + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), abi.encode(3) ); From 0bab786d6a30da8a23e5b1fb6e1a72e74d64e62d Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:52:23 +0200 Subject: [PATCH 62/83] using crosschain batcher --- script/HubManagersDeployer.s.sol | 27 +++++----- src/hub/interfaces/IHub.sol | 2 - src/managers/hub/NAVManager.sol | 2 +- src/managers/hub/SimplePriceManager.sol | 41 +++++++------- test/hub/integration/BaseTest.sol | 2 +- .../managers/hub/integration/NAVManager.t.sol | 27 +++++----- test/managers/hub/unit/NAVManager.t.sol | 5 +- .../hub/unit/SimplePriceManager.t.sol | 54 ++++++++++++------- .../integration/MerkleProofManager.t.sol | 7 ++- .../spoke/integration/OnOfframpManager.t.sol | 5 +- .../spoke/unit/OnOfframpManager.t.sol | 3 +- test/vaults/unit/BatchRequestManager.t.sol | 4 +- 12 files changed, 93 insertions(+), 86 deletions(-) diff --git a/script/HubManagersDeployer.s.sol b/script/HubManagersDeployer.s.sol index 70c8e96c9..a737fe002 100644 --- a/script/HubManagersDeployer.s.sol +++ b/script/HubManagersDeployer.s.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {NAVManager} from "../src/managers/hub/NAVManager.sol"; -import {SimplePriceManager} from "../src/managers/hub/SimplePriceManager.sol"; - import {CommonInput} from "./CommonDeployer.s.sol"; import {HubDeployer, HubReport, HubActionBatcher} from "./HubDeployer.s.sol"; +import {NAVManager} from "../src/managers/hub/NAVManager.sol"; +import {SimplePriceManager} from "../src/managers/hub/SimplePriceManager.sol"; + import "forge-std/Script.sol"; struct HubManagersReport { @@ -21,14 +21,12 @@ contract HubManagersActionBatcher is HubActionBatcher { report.navManager.rely(address(report.hub.common.root)); report.simplePriceManager.rely(address(report.hub.common.root)); - // Rely hub - report.navManager.rely(address(report.hub.hub)); - report.simplePriceManager.rely(address(report.hub.hub)); - // Rely other - report.simplePriceManager.rely(address(report.navManager)); + report.navManager.rely(address(report.hub.hub)); + report.navManager.rely(address(report.hub.hubHandler)); report.navManager.rely(address(report.hub.holdings)); - report.hub.common.gateway.rely(address(report.simplePriceManager)); + report.simplePriceManager.rely(address(report.navManager)); + report.simplePriceManager.rely(address(report.hub.common.crosschainBatcher)); } function revokeManagers(HubManagersReport memory report) public onlyDeployer { @@ -56,11 +54,14 @@ contract HubManagersDeployer is HubDeployer { ) ); - address simplePriceManagerAddr = create3( - generateSalt("simplePriceManager"), - abi.encodePacked(type(SimplePriceManager).creationCode, abi.encode(hub, address(batcher))) + simplePriceManager = SimplePriceManager( + create3( + generateSalt("simplePriceManager"), + abi.encodePacked( + type(SimplePriceManager).creationCode, abi.encode(hub, crosschainBatcher, address(batcher)) + ) + ) ); - simplePriceManager = SimplePriceManager(payable(simplePriceManagerAddr)); batcher.engageManagers(_managersReport()); diff --git a/src/hub/interfaces/IHub.sol b/src/hub/interfaces/IHub.sol index 55202f8ea..b896e1c84 100644 --- a/src/hub/interfaces/IHub.sol +++ b/src/hub/interfaces/IHub.sol @@ -6,7 +6,6 @@ import {IHoldings, HoldingAccount} from "./IHoldings.sol"; import {IAccounting, JournalEntry} from "./IAccounting.sol"; import {IHubRequestManager} from "./IHubRequestManager.sol"; import {IShareClassManager} from "./IShareClassManager.sol"; -import {IHubHelpers} from "./IHubHelpers.sol"; import {D18} from "../../misc/types/D18.sol"; @@ -101,7 +100,6 @@ interface IHub { function holdings() external view returns (IHoldings); function accounting() external view returns (IAccounting); function hubRegistry() external view returns (IHubRegistry); - function hubHelpers() external view returns (IHubHelpers); function sender() external view returns (IHubMessageSender); function shareClassManager() external view returns (IShareClassManager); diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index a9dfba473..c1dfd7587 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -12,8 +12,8 @@ import {IValuation} from "../../common/interfaces/IValuation.sol"; import {ISnapshotHook} from "../../common/interfaces/ISnapshotHook.sol"; import {AccountId, withCentrifugeId, withAssetId} from "../../common/types/AccountId.sol"; -import {IHub, AccountType} from "../../hub/interfaces/IHub.sol"; import {IHoldings} from "../../hub/interfaces/IHoldings.sol"; +import {IHub, AccountType} from "../../hub/interfaces/IHub.sol"; import {IAccounting} from "../../hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 4a1bb1d8a..805404e8b 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -4,24 +4,23 @@ pragma solidity 0.8.28; import {INAVHook} from "./interfaces/INAVManager.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; -import {D18, d18} from "../../misc/types/D18.sol"; -import {IMulticall} from "../../misc/interfaces/IMulticall.sol"; import {Auth} from "../../misc/Auth.sol"; +import {D18, d18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {MAX_MESSAGE_COST} from "../../common/interfaces/IGasService.sol"; -import {IGateway} from "../../common/interfaces/IGateway.sol"; +import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; import {IHub} from "../../hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; + import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; /// @notice Share price calculation manager for single share class pools. contract SimplePriceManager is ISimplePriceManager, Auth { - IGateway public gateway; + ICrosschainBatcher public crosschainBatcher; IHub public immutable hub; IHubRegistry public immutable hubRegistry; IShareClassManager public immutable shareClassManager; @@ -30,9 +29,9 @@ contract SimplePriceManager is ISimplePriceManager, Auth { mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; mapping(PoolId poolId => mapping(address => bool)) public manager; - constructor(IHub hub_, address deployer) Auth(deployer) { + constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) Auth(deployer) { hub = hub_; - gateway = hub_.gateway(); + crosschainBatcher = crosschainBatcher_; hubRegistry = hub_.hubRegistry(); shareClassManager = hub_.shareClassManager(); @@ -57,7 +56,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc ISimplePriceManager function file(bytes32 what, address data) external auth { - if (what == "gateway") gateway = IGateway(data); + if (what == "crosschainBatcher") crosschainBatcher = ICrosschainBatcher(data); else revert ISimplePriceManager.FileUnrecognizedParam(); emit File(what, data); } @@ -85,6 +84,17 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc INAVHook function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { + crosschainBatcher.execute( + abi.encodeWithSignature( + "onUpdateCallback(uint64,bytes16,uint16,uint128)", poolId, scId, centrifugeId, netAssetValue + ) + ); + } + + function onUpdateCallback(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) + external + auth + { NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; Metrics storage metrics_ = metrics[poolId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); @@ -98,7 +108,6 @@ contract SimplePriceManager is ISimplePriceManager, Auth { networkMetrics_.issuance = issuance; uint256 networkCount = metrics_.networks.length; - gateway.startBatching(); hub.updateSharePrice(poolId, scId, price); for (uint256 i; i < networkCount; i++) { @@ -143,7 +152,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); - D18 pricePoolPerAsset = hub.hubHelpers().pricePoolPerAsset(poolId, scId, depositAssetId); + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); D18 navPoolPerShare = _navPerShare(poolId); requestManager.approveDeposits( poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset @@ -166,7 +175,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); - D18 pricePoolPerAsset = hub.hubHelpers().pricePoolPerAsset(poolId, scId, payoutAssetId); + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); D18 navPoolPerShare = _navPerShare(poolId); requestManager.approveRedeems( poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset @@ -179,13 +188,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { //---------------------------------------------------------------------------------------------- function _navPerShare(PoolId poolId) internal view returns (D18) { - return metrics[poolId].issuance == 0 - ? d18(1, 1) - : d18(metrics[poolId].netAssetValue) / d18(metrics[poolId].issuance); - } - - // TODO: remove when not needed anymore - receive() external payable { - // Accept ETH refunds from multicall + Metrics memory metrics_ = metrics[poolId]; + return metrics_.issuance == 0 ? d18(1, 1) : d18(metrics_.netAssetValue) / d18(metrics_.issuance); } } diff --git a/test/hub/integration/BaseTest.sol b/test/hub/integration/BaseTest.sol index 357d88620..f1d4a6016 100644 --- a/test/hub/integration/BaseTest.sol +++ b/test/hub/integration/BaseTest.sol @@ -15,7 +15,7 @@ import {MAX_MESSAGE_COST} from "../../../src/common/interfaces/IGasService.sol"; import {IHubRequestManager} from "../../../src/hub/interfaces/IHubRequestManager.sol"; -import {HubDeployer, HubActionBatcher, CommonInput} from "../../../script/HubDeployer.s.sol"; +import {HubActionBatcher, CommonInput} from "../../../script/HubDeployer.s.sol"; import {ExtendedHubDeployer, ExtendedHubActionBatcher} from "../../../script/ExtendedHubDeployer.s.sol"; import {MockVaults} from "../mocks/MockVaults.sol"; diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index 9e6b8f67c..b453630b3 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -11,8 +11,7 @@ import {ISnapshotHook} from "../../../../src/common/interfaces/ISnapshotHook.sol import "../../../hub/integration/BaseTest.sol"; -import {INAVManager, INAVHook} from "../../../../src/managers/hub/interfaces/INAVManager.sol"; -import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; +import {INAVHook} from "../../../../src/managers/hub/interfaces/INAVManager.sol"; contract NAVManagerIntegrationTest is BaseTest { PoolId constant POOL_A = PoolId.wrap(1); @@ -76,8 +75,6 @@ contract NAVManagerIntegrationTest is BaseTest { valuation.setPrice(POOL_A, scId, asset2, d18(1, 1)); valuation.setPrice(POOL_A, scId, asset3, d18(1, 1)); valuation.setPrice(POOL_A, scId, liabilityAsset, d18(1, 1)); - - vm.deal(address(simplePriceManager), 1 ether); } /// forge-config: default.isolate = true @@ -101,8 +98,8 @@ contract NAVManagerIntegrationTest is BaseTest { vm.stopPrank(); - vm.prank(address(root)); - hub.updateHoldingAmount( + vm.prank(address(messageDispatcher)); + hubHandler.updateHoldingAmount( CHAIN_CP, POOL_A, scId, asset3, uint128(500 * 10 ** asset3Decimals), d18(1, 1), true, false, 0 ); @@ -110,8 +107,8 @@ contract NAVManagerIntegrationTest is BaseTest { vm.expectCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector, POOL_A, scId, CHAIN_CP)); vm.expectCall(address(hub), abi.encodeWithSelector(hub.notifySharePrice.selector, POOL_A, scId, CHAIN_CV)); - vm.prank(address(root)); - hub.updateShares(CHAIN_CP, POOL_A, scId, 500e18, true, true, 1); + vm.prank(address(messageDispatcher)); + hubHandler.updateShares(CHAIN_CP, POOL_A, scId, 500e18, true, true, 1); uint128 navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); uint128 navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); @@ -165,7 +162,7 @@ contract NAVManagerIntegrationTest is BaseTest { assertEq(globalIssuance, 3800e18); vm.prank(address(root)); - hub.initiateTransferShares(CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0); + hubHandler.initiateTransferShares(CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0); navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); @@ -188,8 +185,8 @@ contract NAVManagerIntegrationTest is BaseTest { address(hub), abi.encodeWithSelector(hub.updateSharePrice.selector, POOL_A, scId, d18(3600e18) / d18(3800e18)) ); - vm.prank(address(root)); - hub.updateHoldingAmount(CHAIN_CP, POOL_A, scId, liabilityAsset, 50e18, d18(1, 1), true, true, 2); + vm.prank(address(messageDispatcher)); + hubHandler.updateHoldingAmount(CHAIN_CP, POOL_A, scId, liabilityAsset, 50e18, d18(1, 1), true, true, 2); navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); @@ -208,10 +205,10 @@ contract NAVManagerIntegrationTest is BaseTest { assertEq(globalIssuance, 3800e18); // Decrease liability by paying with a cash asset - vm.prank(address(root)); - hub.updateHoldingAmount(CHAIN_CP, POOL_A, scId, liabilityAsset, 50e18, d18(1, 1), false, false, 3); - vm.prank(address(root)); - hub.updateHoldingAmount( + vm.prank(address(messageDispatcher)); + hubHandler.updateHoldingAmount(CHAIN_CP, POOL_A, scId, liabilityAsset, 50e18, d18(1, 1), false, false, 3); + vm.prank(address(messageDispatcher)); + hubHandler.updateHoldingAmount( CHAIN_CP, POOL_A, scId, asset3, uint128(100 * 10 ** asset3Decimals), d18(1, 2), false, true, 4 ); diff --git a/test/managers/hub/unit/NAVManager.t.sol b/test/managers/hub/unit/NAVManager.t.sol index 2d59b99fc..92a1e3229 100644 --- a/test/managers/hub/unit/NAVManager.t.sol +++ b/test/managers/hub/unit/NAVManager.t.sol @@ -12,12 +12,11 @@ import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; import {AccountId, withCentrifugeId, withAssetId} from "../../../../src/common/types/AccountId.sol"; -import {IHub, AccountType} from "../../../../src/hub/interfaces/IHub.sol"; import {IHoldings} from "../../../../src/hub/interfaces/IHoldings.sol"; +import {NAVManager} from "../../../../src/managers/hub/NAVManager.sol"; +import {IHub, AccountType} from "../../../../src/hub/interfaces/IHub.sol"; import {IAccounting} from "../../../../src/hub/interfaces/IAccounting.sol"; import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; - -import {NAVManager} from "../../../../src/managers/hub/NAVManager.sol"; import {INAVManager, INAVHook} from "../../../../src/managers/hub/interfaces/INAVManager.sol"; import "forge-std/Test.sol"; diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index bb10d0e64..ec568e850 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -6,23 +6,38 @@ import {Multicall} from "../../../../src/misc/Multicall.sol"; import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; +import {ICrosschainBatcher} from "../../../../src/common/interfaces/ICrosschainBatcher.sol"; -import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; -import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; -import {IHubHelpers} from "../../../../src/hub/interfaces/IHubHelpers.sol"; +import {SimplePriceManager} from "../../../../src/managers/hub/SimplePriceManager.sol"; import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; - import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; -import {SimplePriceManager} from "../../../../src/managers/hub/SimplePriceManager.sol"; + +import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; import "forge-std/Test.sol"; contract IsContract {} +contract MockCrosschainBatcher { + function execute(bytes memory data) external payable returns (uint256 cost) { + (bool success, bytes memory returnData) = msg.sender.call{value: msg.value}(data); + if (!success) { + uint256 length = returnData.length; + require(length != 0, "Empty revert"); + + assembly ("memory-safe") { + revert(add(32, returnData), length) + } + } + return 0; + } +} + contract MockHub is Multicall { function notifySharePrice(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external payable {} } @@ -45,6 +60,7 @@ contract SimplePriceManagerTest is Test { address shareClassManager = address(new IsContract()); address batchRequestManager = address(new IsContract()); address hubHelpers = address(new IsContract()); + address crosschainBatcher = address(new MockCrosschainBatcher()); address unauthorized = makeAddr("unauthorized"); address hubManager = makeAddr("hubManager"); @@ -63,9 +79,11 @@ contract SimplePriceManagerTest is Test { vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); vm.mockCall(hub, abi.encodeWithSelector(IHub.gateway.selector), abi.encode(gateway)); - vm.mockCall(hub, abi.encodeWithSelector(IHub.hubHelpers.selector), abi.encode(hubHelpers)); vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); + vm.mockCall( + hub, abi.encodeWithSelector(IHub.pricePoolPerAsset.selector, POOL_A, SC_1, asset1), abi.encode(d18(1, 1)) + ); vm.mockCall(gateway, abi.encodeWithSelector(IGateway.startBatching.selector), abi.encode()); vm.mockCall(gateway, abi.encodeWithSelector(IGateway.endBatching.selector), abi.encode()); @@ -139,18 +157,14 @@ contract SimplePriceManagerTest is Test { abi.encodeWithSelector(IBatchRequestManager.revokeShares.selector), abi.encode(uint256(0)) ); - - vm.mockCall( - hubHelpers, - abi.encodeWithSelector(IHubHelpers.pricePoolPerAsset.selector, POOL_A, SC_1, asset1), - abi.encode(d18(1, 1)) - ); } function _deployManager() internal { - priceManager = new SimplePriceManager(IHub(hub), auth); + priceManager = new SimplePriceManager(IHub(hub), ICrosschainBatcher(crosschainBatcher), auth); vm.prank(auth); priceManager.rely(caller); + vm.prank(auth); + priceManager.rely(crosschainBatcher); vm.prank(hubManager); priceManager.updateManager(POOL_A, manager, true); @@ -241,16 +255,16 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { } contract SimplePriceManagerFileTests is SimplePriceManagerTest { - function testFileGateway() public { - address newGateway = makeAddr("newGateway"); + function testFileCrosschainBatcher() public { + address newCrosschainBatcher = makeAddr("newCrosschainBatcher"); vm.expectEmit(true, false, true, true); - emit ISimplePriceManager.File("gateway", newGateway); + emit ISimplePriceManager.File("crosschainBatcher", newCrosschainBatcher); vm.prank(auth); - priceManager.file("gateway", newGateway); + priceManager.file("crosschainBatcher", newCrosschainBatcher); - assertEq(address(priceManager.gateway()), newGateway); + assertEq(address(priceManager.crosschainBatcher()), newCrosschainBatcher); } function testFileUnrecognizedParam() public { @@ -262,11 +276,11 @@ contract SimplePriceManagerFileTests is SimplePriceManagerTest { } function testFileUnauthorized() public { - address newGateway = makeAddr("newGateway"); + address newCrosschainBatcher = makeAddr("newCrosschainBatcher"); vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - priceManager.file("gateway", newGateway); + priceManager.file("gateway", newCrosschainBatcher); } } diff --git a/test/managers/spoke/integration/MerkleProofManager.t.sol b/test/managers/spoke/integration/MerkleProofManager.t.sol index 684c0f4f4..cf08bfbae 100644 --- a/test/managers/spoke/integration/MerkleProofManager.t.sol +++ b/test/managers/spoke/integration/MerkleProofManager.t.sol @@ -11,14 +11,13 @@ import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import "../../../spoke/integration/BaseTest.sol"; import {BalanceSheet} from "../../../../src/spoke/BalanceSheet.sol"; -import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; - -import {UpdateRestrictionMessageLib} from "../../../../src/hooks/libraries/UpdateRestrictionMessageLib.sol"; - import {VaultDecoder} from "../../../../src/managers/spoke/decoders/VaultDecoder.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; import {MerkleProofManager, PolicyLeaf, Call} from "../../../../src/managers/spoke/MerkleProofManager.sol"; import {IMerkleProofManager, IERC7751} from "../../../../src/managers/spoke/interfaces/IMerkleProofManager.sol"; +import {UpdateRestrictionMessageLib} from "../../../../src/hooks/libraries/UpdateRestrictionMessageLib.sol"; + import {MerkleTreeLib} from "../libraries/MerkleTreeLib.sol"; abstract contract MerkleProofManagerBaseTest is BaseTest { diff --git a/test/managers/spoke/integration/OnOfframpManager.t.sol b/test/managers/spoke/integration/OnOfframpManager.t.sol index 4b08a057f..d228cd09a 100644 --- a/test/managers/spoke/integration/OnOfframpManager.t.sol +++ b/test/managers/spoke/integration/OnOfframpManager.t.sol @@ -9,13 +9,12 @@ import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import "../../../spoke/integration/BaseTest.sol"; +import {OnOfframpManagerFactory} from "../../../../src/managers/spoke/OnOfframpManager.sol"; +import {IOnOfframpManager} from "../../../../src/managers/spoke/interfaces/IOnOfframpManager.sol"; import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; import {UpdateRestrictionMessageLib} from "../../../../src/hooks/libraries/UpdateRestrictionMessageLib.sol"; -import {OnOfframpManagerFactory} from "../../../../src/managers/spoke/OnOfframpManager.sol"; -import {IOnOfframpManager} from "../../../../src/managers/spoke/interfaces/IOnOfframpManager.sol"; - abstract contract OnOfframpManagerBaseTest is BaseTest { using CastLib for *; using UpdateRestrictionMessageLib for *; diff --git a/test/managers/spoke/unit/OnOfframpManager.t.sol b/test/managers/spoke/unit/OnOfframpManager.t.sol index 053145c18..dde386f65 100644 --- a/test/managers/spoke/unit/OnOfframpManager.t.sol +++ b/test/managers/spoke/unit/OnOfframpManager.t.sol @@ -15,10 +15,9 @@ import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {ISpoke} from "../../../../src/spoke/interfaces/ISpoke.sol"; import {IBalanceSheet} from "../../../../src/spoke/interfaces/IBalanceSheet.sol"; import {IUpdateContract} from "../../../../src/spoke/interfaces/IUpdateContract.sol"; -import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; - import {OnOfframpManagerFactory} from "../../../../src/managers/spoke/OnOfframpManager.sol"; import {IOnOfframpManager} from "../../../../src/managers/spoke/interfaces/IOnOfframpManager.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; import {IDepositManager, IWithdrawManager} from "../../../../src/managers/spoke/interfaces/IBalanceSheetManager.sol"; import "forge-std/Test.sol"; diff --git a/test/vaults/unit/BatchRequestManager.t.sol b/test/vaults/unit/BatchRequestManager.t.sol index 868e6eda6..f02ca67dd 100644 --- a/test/vaults/unit/BatchRequestManager.t.sol +++ b/test/vaults/unit/BatchRequestManager.t.sol @@ -20,9 +20,7 @@ import { EpochInvestAmounts, EpochRedeemAmounts, UserOrder, - QueuedOrder, - RequestType, - EpochId + QueuedOrder } from "../../../src/vaults/interfaces/IBatchRequestManager.sol"; import "forge-std/Test.sol"; From 81eb1d5d331741c7bf6566c36cb535254956fbf0 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:03:49 +0200 Subject: [PATCH 63/83] cleanup --- src/managers/hub/SimplePriceManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 805404e8b..28184bc81 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -85,8 +85,8 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc INAVHook function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { crosschainBatcher.execute( - abi.encodeWithSignature( - "onUpdateCallback(uint64,bytes16,uint16,uint128)", poolId, scId, centrifugeId, netAssetValue + abi.encodeWithSelector( + SimplePriceManager.onUpdateCallback.selector, poolId, scId, centrifugeId, netAssetValue ) ); } From 8c34beea54d9ab181e56f0b2d18b75b73dda7e29 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:14:26 +0200 Subject: [PATCH 64/83] add separate approve/issue methods --- src/managers/hub/SimplePriceManager.sol | 75 +++++++++ .../hub/interfaces/ISimplePriceManager.sol | 23 ++- .../managers/hub/integration/NAVManager.t.sol | 20 +-- .../hub/unit/SimplePriceManager.t.sol | 143 +++++++++++++++++- 4 files changed, 240 insertions(+), 21 deletions(-) diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 28184bc81..4d7e027db 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -84,6 +84,11 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc INAVHook function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { + NetworkMetrics memory networkMetrics_ = networkMetrics[poolId][centrifugeId]; + + // If there are pending epochs to be issued or revoked, skip updating the share price, as it will likely be off + if (networkMetrics_.issueEpochsBehind > 0 || networkMetrics_.revokeEpochsBehind > 0) return; + crosschainBatcher.execute( abi.encodeWithSelector( SimplePriceManager.onUpdateCallback.selector, poolId, scId, centrifugeId, netAssetValue @@ -137,6 +142,76 @@ contract SimplePriceManager is ISimplePriceManager, Auth { // Manager actions //---------------------------------------------------------------------------------------------- + function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) + external + onlyManager(poolId) + { + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + + networkMetrics_.issueEpochsBehind++; + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); + requestManager.approveDeposits( + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset + ); + } + + function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) + external + onlyManager(poolId) + { + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + + require(networkMetrics_.issueEpochsBehind > 0, MismatchedEpochs()); + networkMetrics_.issueEpochsBehind--; + + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + } + + function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) + external + onlyManager(poolId) + { + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; + + networkMetrics_.revokeEpochsBehind++; + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); + requestManager.approveRedeems( + poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset + ); + } + + function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) + external + onlyManager(poolId) + { + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; + + require(networkMetrics_.revokeEpochsBehind > 0, MismatchedEpochs()); + networkMetrics_.revokeEpochsBehind--; + + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + } + /// @inheritdoc ISimplePriceManager function approveDepositsAndIssueShares( PoolId poolId, diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index c639f0787..576385997 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -30,6 +30,8 @@ interface ISimplePriceManager is INAVHook { struct NetworkMetrics { uint128 netAssetValue; uint128 issuance; + uint32 issueEpochsBehind; + uint32 revokeEpochsBehind; } function metrics(PoolId poolId) external view returns (uint128 netAssetValue, uint128 issuance); @@ -37,7 +39,7 @@ interface ISimplePriceManager is INAVHook { function networkMetrics(PoolId poolId, uint16 centrifugeId) external view - returns (uint128 netAssetValue, uint128 issuance); + returns (uint128 netAssetValue, uint128 issuance, uint32 issueEpochsBehind, uint32 revokeEpochsBehind); function manager(PoolId poolId, address manager_) external view returns (bool); //---------------------------------------------------------------------------------------------- @@ -67,7 +69,7 @@ interface ISimplePriceManager is INAVHook { /// @param scId The share class ID /// @param depositAssetId The asset ID for deposits /// @param approvedAssetAmount Amount of assets to approve - /// @param extraGasLimit Extra gas limit for cross-chain operations + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain function approveDepositsAndIssueShares( PoolId poolId, ShareClassId scId, @@ -76,12 +78,27 @@ interface ISimplePriceManager is INAVHook { uint128 extraGasLimit ) external; + /// @notice Approve redemption requests for a given share amount + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param approvedShareAmount Amount of shares to approve for redemption + function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) + external; + + /// @notice Revoke shares from approved redemption requests + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external; + /// @notice Approve redeems and revoke shares in sequence using current NAV per share /// @param poolId The pool ID /// @param scId The share class ID /// @param payoutAssetId The asset ID for payouts /// @param approvedShareAmount Amount of shares to approve for redemption - /// @param extraGasLimit Extra gas limit for cross-chain operations + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain function approveRedeemsAndRevokeShares( PoolId poolId, ShareClassId scId, diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index b453630b3..c12ddc08e 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -112,8 +112,8 @@ contract NAVManagerIntegrationTest is BaseTest { uint128 navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); uint128 navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (uint128 navHub2, uint128 issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); - (uint128 navSpoke2, uint128 issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (uint128 navHub2, uint128 issuanceHub,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (uint128 navSpoke2, uint128 issuanceSpoke,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); (uint128 globalNAV, uint128 globalIssuance) = simplePriceManager.metrics(POOL_A); assertEq(navHub, 500e18); @@ -140,8 +140,8 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (navHub2, issuanceHub,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); (bool spokeGainIsPositive, uint128 spokeGain) = accounting.accountValue(POOL_A, navManager.gainAccount(CHAIN_CV)); @@ -166,8 +166,8 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (navHub2, issuanceHub,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); // NAV and global issuance should remain unchanged, only issuance per network changes @@ -190,8 +190,8 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (navHub2, issuanceHub,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); // Liability reduces the NAV @@ -214,8 +214,8 @@ contract NAVManagerIntegrationTest is BaseTest { navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); - (navHub2, issuanceHub) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); - (navSpoke2, issuanceSpoke) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); + (navHub2, issuanceHub,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CP); + (navSpoke2, issuanceSpoke,,) = simplePriceManager.networkMetrics(POOL_A, CHAIN_CV); (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); // NAV should remain unchanged diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index ec568e850..a4460d383 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -320,7 +320,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { assertEq(globalIssuance, 100); assertEq(globalNAV, netAssetValue); - (uint128 networkNAV, uint128 networkIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + (uint128 networkNAV, uint128 networkIssuance,,) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); assertEq(networkNAV, netAssetValue); assertEq(networkIssuance, 100); } @@ -412,8 +412,8 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); - (uint128 fromNAV, uint128 fromIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - (uint128 toNAV, uint128 toIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); + (uint128 fromNAV, uint128 fromIssuance,,) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + (uint128 toNAV, uint128 toIssuance,,) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); assertEq(fromIssuance, 50); // 100 - 50 assertEq(toIssuance, 250); // 200 + 50 @@ -433,8 +433,8 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 0); - (, uint128 fromIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - (, uint128 toIssuance) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); + (, uint128 fromIssuance,,) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + (, uint128 toIssuance,,) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); assertEq(fromIssuance, 100); assertEq(toIssuance, 200); @@ -442,6 +442,8 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { } contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { + D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 + function setUp() public override { super.setUp(); @@ -452,7 +454,6 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { function testApproveDepositsAndIssueSharesSuccess() public { uint128 approvedAssetAmount = 500; uint128 extraGasLimit = 100000; - D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 vm.expectCall( address(batchRequestManager), @@ -519,8 +520,14 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { vm.expectCall( address(batchRequestManager), abi.encodeWithSelector( - IBatchRequestManager.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit - ) // 1000/100 = 10 + IBatchRequestManager.revokeShares.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + expectedNavPerShare, + extraGasLimit + ) ); vm.prank(manager); @@ -549,4 +556,124 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { vm.prank(manager); priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); } + + function testApproveRedeemsSuccess() public { + uint128 approvedShareAmount = 50; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveRedeems.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + approvedShareAmount, + d18(1, 1) + ) + ); + + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_1, asset1, approvedShareAmount); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(revokeEpochsBehind, 1); + assertEq(issueEpochsBehind, 0); + } + + function testApproveRedeemsUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); + } + + function testRevokeSharesSuccess() public { + uint128 extraGasLimit = 100000; + + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_1, asset1, extraGasLimit); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(revokeEpochsBehind, 0); + assertEq(issueEpochsBehind, 0); + } + + function testRevokeSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); + } + + function testRevokeSharesWithoutPendingEpochs() public { + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); + } + + function testApproveDepositsSuccess() public { + uint128 approvedAssetAmount = 500; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveDeposits.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + approvedAssetAmount, + d18(1, 1) + ) + ); + + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_1, asset1, approvedAssetAmount); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(issueEpochsBehind, 1); + assertEq(revokeEpochsBehind, 0); + } + + function testIssueSharesSuccess() public { + uint128 extraGasLimit = 100000; + + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_1, asset1, 500); + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.issueShares.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + expectedNavPerShare, + extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_1, asset1, extraGasLimit); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(issueEpochsBehind, 0); + assertEq(revokeEpochsBehind, 0); + } + + function testIssueSharesWithoutPendingEpochs() public { + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_1, asset1, 100000); + } } From 0080e3af64e9d974d341ac882da024b9deaa5ac0 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:58:13 +0200 Subject: [PATCH 65/83] event --- src/managers/hub/SimplePriceManager.sol | 1 + src/managers/hub/interfaces/ISimplePriceManager.sol | 1 + 2 files changed, 2 insertions(+) diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 4d7e027db..b536fb43a 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -69,6 +69,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc ISimplePriceManager function setNetworks(PoolId poolId, uint16[] calldata centrifugeIds) external onlyHubManager(poolId) { metrics[poolId].networks = centrifugeIds; + emit SetNetworks(poolId, centrifugeIds); } /// @inheritdoc ISimplePriceManager diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index 576385997..c1289a8c0 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -15,6 +15,7 @@ interface ISimplePriceManager is INAVHook { PoolId indexed poolId, uint16 indexed fromCentrifugeId, uint16 indexed toCentrifugeId, uint128 sharesTransferred ); event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); + event SetNetworks(PoolId indexed poolId, uint16[] networks); event File(bytes32 indexed what, address data); error InvalidShareClassCount(); From a92f592f60d2e163a8a23360f79b2cf006d7bce6 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:29:44 +0200 Subject: [PATCH 66/83] consts --- src/managers/hub/NAVManager.sol | 11 +++++------ test/managers/hub/unit/NAVManager.t.sol | 8 ++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index c1dfd7587..e2791b7ff 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -168,9 +168,8 @@ contract NAVManager is INAVManager, Auth { external onlyManager(poolId) { + // TODO: Should we have this funtion at all? Seems like this can only mess up the accounting. hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); - // TODO: update assetIdToAccountId mapping and update value? - // Do we need to do something with the old account/value? } /// @inheritdoc INAVManager @@ -236,22 +235,22 @@ contract NAVManager is INAVManager, Auth { /// @inheritdoc INAVManager function equityAccount(uint16 centrifugeId) public pure returns (AccountId) { - return withCentrifugeId(centrifugeId, 1); + return withCentrifugeId(centrifugeId, uint16(AccountType.Equity)); } /// @inheritdoc INAVManager function liabilityAccount(uint16 centrifugeId) public pure returns (AccountId) { - return withCentrifugeId(centrifugeId, 2); + return withCentrifugeId(centrifugeId, uint16(AccountType.Liability)); } /// @inheritdoc INAVManager function gainAccount(uint16 centrifugeId) public pure returns (AccountId) { - return withCentrifugeId(centrifugeId, 3); + return withCentrifugeId(centrifugeId, uint16(AccountType.Gain)); } /// @inheritdoc INAVManager function lossAccount(uint16 centrifugeId) public pure returns (AccountId) { - return withCentrifugeId(centrifugeId, 4); + return withCentrifugeId(centrifugeId, uint16(AccountType.Loss)); } //---------------------------------------------------------------------------------------------- diff --git a/test/managers/hub/unit/NAVManager.t.sol b/test/managers/hub/unit/NAVManager.t.sol index 92a1e3229..c3d6b7eee 100644 --- a/test/managers/hub/unit/NAVManager.t.sol +++ b/test/managers/hub/unit/NAVManager.t.sol @@ -607,25 +607,25 @@ contract NAVManagerCloseGainLossTest is NAVManagerTest { contract NAVManagerHelperFunctionsTest is NAVManagerTest { function testEquityAccount() public view { - AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 1); + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, uint16(AccountType.Equity)); AccountId actual = navManager.equityAccount(CENTRIFUGE_ID_1); assertEq(actual.raw(), expected.raw()); } function testLiabilityAccount() public view { - AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 2); + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, uint16(AccountType.Liability)); AccountId actual = navManager.liabilityAccount(CENTRIFUGE_ID_1); assertEq(actual.raw(), expected.raw()); } function testGainAccount() public view { - AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 3); + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, uint16(AccountType.Gain)); AccountId actual = navManager.gainAccount(CENTRIFUGE_ID_1); assertEq(actual.raw(), expected.raw()); } function testLossAccount() public view { - AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, 4); + AccountId expected = withCentrifugeId(CENTRIFUGE_ID_1, uint16(AccountType.Loss)); AccountId actual = navManager.lossAccount(CENTRIFUGE_ID_1); assertEq(actual.raw(), expected.raw()); } From a5e583caed858725d3f941f15c85711eec6dcfc4 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:51:44 +0200 Subject: [PATCH 67/83] invalid share class --- src/common/types/ShareClassId.sol | 10 ++- src/managers/hub/SimplePriceManager.sol | 16 +++-- .../hub/interfaces/ISimplePriceManager.sol | 1 + .../hub/unit/SimplePriceManager.t.sol | 65 ++++++++++++++++++- 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/common/types/ShareClassId.sol b/src/common/types/ShareClassId.sol index 3d2e17875..277f4b00c 100644 --- a/src/common/types/ShareClassId.sol +++ b/src/common/types/ShareClassId.sol @@ -9,6 +9,10 @@ function isNull(ShareClassId scId) pure returns (bool) { return ShareClassId.unwrap(scId) == 0; } +function index(ShareClassId scId) pure returns (uint32) { + return uint32(uint128(ShareClassId.unwrap(scId))); +} + function equals(ShareClassId left, ShareClassId right) pure returns (bool) { return ShareClassId.unwrap(left) == ShareClassId.unwrap(right); } @@ -17,8 +21,8 @@ function raw(ShareClassId scId) pure returns (bytes16) { return ShareClassId.unwrap(scId); } -function newShareClassId(PoolId poolId, uint32 index) pure returns (ShareClassId scId) { - return ShareClassId.wrap(bytes16((uint128(PoolId.unwrap(poolId)) << 64) + index)); +function newShareClassId(PoolId poolId, uint32 index_) pure returns (ShareClassId scId) { + return ShareClassId.wrap(bytes16((uint128(PoolId.unwrap(poolId)) << 64) + index_)); } -using {isNull, raw, equals as ==} for ShareClassId global; +using {isNull, index, raw, equals as ==} for ShareClassId global; diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index b536fb43a..afcefb696 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -34,10 +34,6 @@ contract SimplePriceManager is ISimplePriceManager, Auth { crosschainBatcher = crosschainBatcher_; hubRegistry = hub_.hubRegistry(); shareClassManager = hub_.shareClassManager(); - - // TODO: where to check share class count? - // require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); - // scId = shareClassManager.previewShareClassId(poolId, 1); } modifier onlyManager(PoolId poolId) { @@ -68,6 +64,8 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc ISimplePriceManager function setNetworks(PoolId poolId, uint16[] calldata centrifugeIds) external onlyHubManager(poolId) { + require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); + metrics[poolId].networks = centrifugeIds; emit SetNetworks(poolId, centrifugeIds); } @@ -85,6 +83,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc INAVHook function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { + require(scId.index() == 1, InvalidShareClass()); NetworkMetrics memory networkMetrics_ = networkMetrics[poolId][centrifugeId]; // If there are pending epochs to be issued or revoked, skip updating the share price, as it will likely be off @@ -126,11 +125,12 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc INAVHook function onTransfer( PoolId poolId, - ShareClassId, + ShareClassId scId, uint16 fromCentrifugeId, uint16 toCentrifugeId, uint128 sharesTransferred ) external auth { + require(scId.index() == 1, InvalidShareClass()); NetworkMetrics storage fromMetrics = networkMetrics[poolId][fromCentrifugeId]; NetworkMetrics storage toMetrics = networkMetrics[poolId][toCentrifugeId]; fromMetrics.issuance -= sharesTransferred; @@ -147,6 +147,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); @@ -165,6 +166,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); @@ -182,6 +184,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); @@ -200,6 +203,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); @@ -221,6 +225,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { uint128 approvedAssetAmount, uint128 extraGasLimit ) external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); @@ -244,6 +249,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { uint128 approvedShareAmount, uint128 extraGasLimit ) external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index c1289a8c0..1d4ceb5a6 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -19,6 +19,7 @@ interface ISimplePriceManager is INAVHook { event File(bytes32 indexed what, address data); error InvalidShareClassCount(); + error InvalidShareClass(); error MismatchedEpochs(); error FileUnrecognizedParam(); diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index a4460d383..75b3cb3e9 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -7,7 +7,7 @@ import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; import {PoolId} from "../../../../src/common/types/PoolId.sol"; import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; -import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; +import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; import {ICrosschainBatcher} from "../../../../src/common/interfaces/ICrosschainBatcher.sol"; @@ -45,8 +45,8 @@ contract MockHub is Multicall { contract SimplePriceManagerTest is Test { PoolId constant POOL_A = PoolId.wrap(1); PoolId constant POOL_B = PoolId.wrap(2); - ShareClassId constant SC_1 = ShareClassId.wrap(bytes16("1")); - ShareClassId constant SC_2 = ShareClassId.wrap(bytes16("2")); + ShareClassId immutable SC_1 = newShareClassId(POOL_A, 1); + ShareClassId immutable SC_2 = newShareClassId(POOL_A, 2); uint16 constant CENTRIFUGE_ID_1 = 1; uint16 constant CENTRIFUGE_ID_2 = 2; uint16 constant CENTRIFUGE_ID_3 = 3; @@ -191,6 +191,9 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { networks[1] = CENTRIFUGE_ID_2; networks[2] = CENTRIFUGE_ID_3; + vm.expectEmit(true, true, true, true); + emit ISimplePriceManager.SetNetworks(POOL_A, networks); + vm.prank(hubManager); priceManager.setNetworks(POOL_A, networks); @@ -217,6 +220,24 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { priceManager.setNetworks(POOL_A, networks); } + function testSetNetworksInvalidShareClassCount() public { + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_B), + abi.encode(2) + ); + vm.mockCall( + hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_B, hubManager), abi.encode(true) + ); + + uint16[] memory networks = new uint16[](1); + networks[0] = CENTRIFUGE_ID_1; + + vm.expectRevert(ISimplePriceManager.InvalidShareClassCount.selector); + vm.prank(hubManager); + priceManager.setNetworks(POOL_B, networks); + } + function testUpdateManagerSuccess() public { address newManager = makeAddr("newManager"); @@ -390,6 +411,12 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { assertEq(globalIssuance, 0); assertEq(globalNAV, 1000); } + + function testInvalidShareClass() public { + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_2, CENTRIFUGE_ID_1, 1000); + } } contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { @@ -439,6 +466,12 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { assertEq(fromIssuance, 100); assertEq(toIssuance, 200); } + + function testInvalidShareClass() public { + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(caller); + priceManager.onTransfer(POOL_A, SC_2, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); + } } contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { @@ -676,4 +709,30 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { vm.prank(manager); priceManager.issueShares(POOL_A, SC_1, asset1, 100000); } + + function testInvalidShareClass() public { + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_2, asset1, 1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_2, asset1, 1, 1); + } } From a1f3c80b92219ed47b21363c30d46bcc3989a046 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:55:05 +0200 Subject: [PATCH 68/83] spelling --- src/managers/hub/NAVManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index e2791b7ff..1a123c899 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -168,7 +168,7 @@ contract NAVManager is INAVManager, Auth { external onlyManager(poolId) { - // TODO: Should we have this funtion at all? Seems like this can only mess up the accounting. + // TODO: Should we have this function at all? Seems like this can only mess up the accounting. hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); } From 41b2366683222f3fa31de77d88351428c497feb7 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:20:24 +0200 Subject: [PATCH 69/83] addNetwork + removeNetwork --- src/managers/hub/SimplePriceManager.sol | 34 ++++- .../hub/interfaces/ISimplePriceManager.sol | 56 +++++--- .../managers/hub/integration/NAVManager.t.sol | 6 +- .../hub/unit/SimplePriceManager.t.sol | 121 +++++++++++++----- 4 files changed, 159 insertions(+), 58 deletions(-) diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index afcefb696..35845962a 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -63,11 +63,35 @@ contract SimplePriceManager is ISimplePriceManager, Auth { } /// @inheritdoc ISimplePriceManager - function setNetworks(PoolId poolId, uint16[] calldata centrifugeIds) external onlyHubManager(poolId) { + function addNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); - metrics[poolId].networks = centrifugeIds; - emit SetNetworks(poolId, centrifugeIds); + metrics[poolId].networks.push(centrifugeId); + emit UpdateNetworks(poolId, metrics[poolId].networks); + } + + /// @inheritdoc ISimplePriceManager + function removeNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { + uint16[] storage networks_ = metrics[poolId].networks; + uint256 length = networks_.length; + for (uint256 i; i < length; i++) { + if (networks_[i] == centrifugeId) { + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; + Metrics storage metrics_ = metrics[poolId]; + + metrics_.netAssetValue -= networkMetrics_.netAssetValue; + metrics_.issuance -= networkMetrics_.issuance; + + delete networkMetrics[poolId][centrifugeId]; + + networks_[i] = networks_[length - 1]; + networks_.pop(); + + emit UpdateNetworks(poolId, networks_); + return; + } + } + revert NetworkNotFound(); } /// @inheritdoc ISimplePriceManager @@ -119,7 +143,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { hub.notifySharePrice(poolId, scId, metrics_.networks[i]); } - emit Update(poolId, metrics_.netAssetValue, metrics_.issuance, price); + emit Update(poolId, scId, metrics_.netAssetValue, metrics_.issuance, price); } /// @inheritdoc INAVHook @@ -136,7 +160,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { fromMetrics.issuance -= sharesTransferred; toMetrics.issuance += sharesTransferred; - emit Transfer(poolId, fromCentrifugeId, toCentrifugeId, sharesTransferred); + emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); } //---------------------------------------------------------------------------------------------- diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index 1d4ceb5a6..ee4eaff7d 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -10,18 +10,23 @@ import {AssetId} from "../../../common/types/AssetId.sol"; import {ShareClassId} from "../../../common/types/ShareClassId.sol"; interface ISimplePriceManager is INAVHook { - event Update(PoolId indexed poolId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); + event Update(PoolId indexed poolId, ShareClassId scId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); event Transfer( - PoolId indexed poolId, uint16 indexed fromCentrifugeId, uint16 indexed toCentrifugeId, uint128 sharesTransferred + PoolId indexed poolId, + ShareClassId scId, + uint16 indexed fromCentrifugeId, + uint16 indexed toCentrifugeId, + uint128 sharesTransferred ); event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); - event SetNetworks(PoolId indexed poolId, uint16[] networks); + event UpdateNetworks(PoolId indexed poolId, uint16[] networks); event File(bytes32 indexed what, address data); error InvalidShareClassCount(); error InvalidShareClass(); error MismatchedEpochs(); error FileUnrecognizedParam(); + error NetworkNotFound(); struct Metrics { uint128 netAssetValue; @@ -48,11 +53,15 @@ interface ISimplePriceManager is INAVHook { // Administration //---------------------------------------------------------------------------------------------- - /// @notice Update the list of networks the pool is active on - /// @dev Ensure the number of network updates can fit in a single block + /// @notice Add a network to the pool + /// @param poolId The pool ID + /// @param centrifugeId Centrifuge ID for the network to add + function addNetwork(PoolId poolId, uint16 centrifugeId) external; + + /// @notice Remove a network from the pool /// @param poolId The pool ID - /// @param centrifugeIds Array of Centrifuge IDs for networks - function setNetworks(PoolId poolId, uint16[] calldata centrifugeIds) external; + /// @param centrifugeId Centrifuge ID for the network to remove + function removeNetwork(PoolId poolId, uint16 centrifugeId) external; /// @notice Update whether an address can manage the NAV manager /// @param poolId The pool ID @@ -66,19 +75,20 @@ interface ISimplePriceManager is INAVHook { // Manager actions //---------------------------------------------------------------------------------------------- - /// @notice Approve deposits and issue shares in sequence using current NAV per share + /// @notice Approve deposit requests for a given asset amount + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param approvedAssetAmount Amount of assets to approve for deposit + function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) + external; + + /// @notice Issue shares from approved deposit requests /// @param poolId The pool ID /// @param scId The share class ID /// @param depositAssetId The asset ID for deposits - /// @param approvedAssetAmount Amount of assets to approve /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function approveDepositsAndIssueShares( - PoolId poolId, - ShareClassId scId, - AssetId depositAssetId, - uint128 approvedAssetAmount, - uint128 extraGasLimit - ) external; + function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external; /// @notice Approve redemption requests for a given share amount /// @param poolId The pool ID @@ -95,6 +105,20 @@ interface ISimplePriceManager is INAVHook { /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external; + /// @notice Approve deposits and issue shares in sequence using current NAV per share + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param approvedAssetAmount Amount of assets to approve + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function approveDepositsAndIssueShares( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external; + /// @notice Approve redeems and revoke shares in sequence using current NAV per share /// @param poolId The pool ID /// @param scId The share class ID diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index c12ddc08e..edc73a670 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -64,10 +64,8 @@ contract NAVManagerIntegrationTest is BaseTest { navManager.setNAVHook(POOL_A, INAVHook(address(simplePriceManager))); - uint16[] memory networks = new uint16[](2); - networks[0] = CHAIN_CP; - networks[1] = CHAIN_CV; - simplePriceManager.setNetworks(POOL_A, networks); + simplePriceManager.addNetwork(POOL_A, CHAIN_CP); + simplePriceManager.addNetwork(POOL_A, CHAIN_CV); vm.stopPrank(); diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index 75b3cb3e9..fe79a5abc 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -185,42 +185,43 @@ contract SimplePriceManagerConstructorTest is SimplePriceManagerTest { } contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { - function testSetNetworksSuccess() public { - uint16[] memory networks = new uint16[](3); + function testAddNetworkSuccess() public { + uint16[] memory networks = new uint16[](1); networks[0] = CENTRIFUGE_ID_1; - networks[1] = CENTRIFUGE_ID_2; - networks[2] = CENTRIFUGE_ID_3; vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.SetNetworks(POOL_A, networks); + emit ISimplePriceManager.UpdateNetworks(POOL_A, networks); vm.prank(hubManager); - priceManager.setNetworks(POOL_A, networks); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); uint16[] memory storedNetworks = priceManager.networks(POOL_A); + assertEq(storedNetworks.length, 1); + assertEq(storedNetworks[0], CENTRIFUGE_ID_1); + + uint16[] memory networks2 = new uint16[](2); + networks2[0] = CENTRIFUGE_ID_1; + networks2[1] = CENTRIFUGE_ID_2; + + vm.expectEmit(true, true, true, true); + emit ISimplePriceManager.UpdateNetworks(POOL_A, networks2); + + vm.prank(hubManager); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_2); + storedNetworks = priceManager.networks(POOL_A); + assertEq(storedNetworks.length, 2); assertEq(storedNetworks[0], CENTRIFUGE_ID_1); assertEq(storedNetworks[1], CENTRIFUGE_ID_2); - assertEq(storedNetworks[2], CENTRIFUGE_ID_3); } - function testSetNetworksUnauthorized() public { - uint16[] memory networks = new uint16[](1); - networks[0] = CENTRIFUGE_ID_1; - + function testAddNetworkUnauthorized() public { vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - priceManager.setNetworks(POOL_A, networks); - } - - function testSetNetworksEmpty() public { - uint16[] memory networks = new uint16[](0); - - vm.prank(hubManager); - priceManager.setNetworks(POOL_A, networks); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); } - function testSetNetworksInvalidShareClassCount() public { + function testAddNetworkInvalidShareClassCount() public { vm.mockCall( shareClassManager, abi.encodeWithSelector(IShareClassManager.shareClassCount.selector, POOL_B), @@ -230,12 +231,68 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_B, hubManager), abi.encode(true) ); - uint16[] memory networks = new uint16[](1); - networks[0] = CENTRIFUGE_ID_1; - vm.expectRevert(ISimplePriceManager.InvalidShareClassCount.selector); vm.prank(hubManager); - priceManager.setNetworks(POOL_B, networks); + priceManager.addNetwork(POOL_B, CENTRIFUGE_ID_1); + } + + function testRemoveNetworkSuccess() public { + vm.prank(hubManager); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); + vm.prank(hubManager); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_2); + vm.prank(hubManager); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_3); + + uint16[] memory storedNetworks = priceManager.networks(POOL_A); + assertEq(storedNetworks.length, 3); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, 2000); + + (uint128 globalNAV, uint128 globalIssuance) = priceManager.metrics(POOL_A); + assertEq(globalNAV, 3000); + assertEq(globalIssuance, 300); + + (uint128 network2NAV, uint128 network2Issuance,,) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); + assertEq(network2NAV, 2000); + assertEq(network2Issuance, 200); + + vm.prank(hubManager); + priceManager.removeNetwork(POOL_A, CENTRIFUGE_ID_2); + + storedNetworks = priceManager.networks(POOL_A); + assertEq(storedNetworks.length, 2); + assertEq(storedNetworks[0], CENTRIFUGE_ID_1); + assertEq(storedNetworks[1], CENTRIFUGE_ID_3); + + (globalNAV, globalIssuance) = priceManager.metrics(POOL_A); + assertEq(globalNAV, 1000); + assertEq(globalIssuance, 100); + + (network2NAV, network2Issuance,,) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_2); + assertEq(network2NAV, 0); + assertEq(network2Issuance, 0); + } + + function testRemoveNetworkUnauthorized() public { + vm.prank(hubManager); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); + + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.removeNetwork(POOL_A, CENTRIFUGE_ID_1); + } + + function testRemoveNetworkNotFound() public { + vm.prank(hubManager); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); + + vm.expectRevert(ISimplePriceManager.NetworkNotFound.selector); + vm.prank(hubManager); + priceManager.removeNetwork(POOL_A, CENTRIFUGE_ID_2); } function testUpdateManagerSuccess() public { @@ -309,12 +366,10 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { function setUp() public override { super.setUp(); - uint16[] memory networks = new uint16[](2); - networks[0] = CENTRIFUGE_ID_1; - networks[1] = CENTRIFUGE_ID_2; - vm.prank(hubManager); - priceManager.setNetworks(POOL_A, networks); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); + vm.prank(hubManager); + priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_2); } function testOnUpdateFirstUpdate() public { @@ -332,7 +387,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { ); vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(POOL_A, netAssetValue, 100, d18(10, 1)); + emit ISimplePriceManager.Update(POOL_A, SC_1, netAssetValue, 100, d18(10, 1)); vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, netAssetValue); @@ -356,7 +411,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateSharePrice.selector, POOL_A, SC_1, d18(9, 1))); vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(POOL_A, 2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 + emit ISimplePriceManager.Update(POOL_A, SC_1, 2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, netAssetValue2); @@ -379,7 +434,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { uint128 newNetAssetValue = 1200; vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(POOL_A, 1200, 150, d18(8, 1)); // 1200/150 = 8 + emit ISimplePriceManager.Update(POOL_A, SC_1, 1200, 150, d18(8, 1)); // 1200/150 = 8 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, newNetAssetValue); @@ -434,7 +489,7 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { uint128 sharesTransferred = 50; vm.expectEmit(true, true, false, true); - emit ISimplePriceManager.Transfer(POOL_A, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); + emit ISimplePriceManager.Transfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); From 09429e37bd7df97df868a71c23c3a998b9830120 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:58:13 +0200 Subject: [PATCH 70/83] split SimplePriceManager --- src/managers/hub/SimplePriceManager.sol | 162 ++--------------- src/managers/hub/SimplePriceManagerBase.sol | 172 ++++++++++++++++++ .../hub/interfaces/ISimplePriceManager.sol | 69 +------ .../interfaces/ISimplePriceManagerBase.sol | 73 ++++++++ .../hub/unit/SimplePriceManager.t.sol | 51 +++--- 5 files changed, 291 insertions(+), 236 deletions(-) create mode 100644 src/managers/hub/SimplePriceManagerBase.sol create mode 100644 src/managers/hub/interfaces/ISimplePriceManagerBase.sol diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 35845962a..96a383812 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.28; import {INAVHook} from "./interfaces/INAVManager.sol"; +import {SimplePriceManagerBase} from "./SimplePriceManagerBase.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; -import {Auth} from "../../misc/Auth.sol"; import {D18, d18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; @@ -13,160 +13,38 @@ import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; import {IHub} from "../../hub/interfaces/IHub.sol"; -import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; -import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; -/// @notice Share price calculation manager for single share class pools. -contract SimplePriceManager is ISimplePriceManager, Auth { - ICrosschainBatcher public crosschainBatcher; - IHub public immutable hub; - IHubRegistry public immutable hubRegistry; - IShareClassManager public immutable shareClassManager; - - mapping(PoolId poolId => Metrics) public metrics; - mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; - mapping(PoolId poolId => mapping(address => bool)) public manager; - - constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) Auth(deployer) { - hub = hub_; - crosschainBatcher = crosschainBatcher_; - hubRegistry = hub_.hubRegistry(); - shareClassManager = hub_.shareClassManager(); - } - - modifier onlyManager(PoolId poolId) { - require(manager[poolId][msg.sender], NotAuthorized()); - _; - } - - modifier onlyHubManager(PoolId poolId) { - require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); - _; - } - - //---------------------------------------------------------------------------------------------- - // Administration - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc ISimplePriceManager - function file(bytes32 what, address data) external auth { - if (what == "crosschainBatcher") crosschainBatcher = ICrosschainBatcher(data); - else revert ISimplePriceManager.FileUnrecognizedParam(); - emit File(what, data); - } - - /// @inheritdoc ISimplePriceManager - function networks(PoolId poolId) external view returns (uint16[] memory) { - return metrics[poolId].networks; - } - - /// @inheritdoc ISimplePriceManager - function addNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { - require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); - - metrics[poolId].networks.push(centrifugeId); - emit UpdateNetworks(poolId, metrics[poolId].networks); - } - - /// @inheritdoc ISimplePriceManager - function removeNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { - uint16[] storage networks_ = metrics[poolId].networks; - uint256 length = networks_.length; - for (uint256 i; i < length; i++) { - if (networks_[i] == centrifugeId) { - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; - Metrics storage metrics_ = metrics[poolId]; - - metrics_.netAssetValue -= networkMetrics_.netAssetValue; - metrics_.issuance -= networkMetrics_.issuance; - - delete networkMetrics[poolId][centrifugeId]; - - networks_[i] = networks_[length - 1]; - networks_.pop(); - - emit UpdateNetworks(poolId, networks_); - return; - } - } - revert NetworkNotFound(); - } - - /// @inheritdoc ISimplePriceManager - function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { - manager[poolId][manager_] = canManage; - - emit UpdateManager(poolId, manager_, canManage); - } +/// @notice Simple price manager for single share class pools with async request management. +contract SimplePriceManager is SimplePriceManagerBase, ISimplePriceManager { + constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) + SimplePriceManagerBase(hub_, crosschainBatcher_, deployer) + {} //---------------------------------------------------------------------------------------------- // Updates //---------------------------------------------------------------------------------------------- - /// @inheritdoc INAVHook - function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external auth { - require(scId.index() == 1, InvalidShareClass()); + /// @inheritdoc SimplePriceManagerBase + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) + public + override(SimplePriceManagerBase, INAVHook) + auth + { NetworkMetrics memory networkMetrics_ = networkMetrics[poolId][centrifugeId]; // If there are pending epochs to be issued or revoked, skip updating the share price, as it will likely be off if (networkMetrics_.issueEpochsBehind > 0 || networkMetrics_.revokeEpochsBehind > 0) return; - crosschainBatcher.execute( - abi.encodeWithSelector( - SimplePriceManager.onUpdateCallback.selector, poolId, scId, centrifugeId, netAssetValue - ) - ); - } - - function onUpdateCallback(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) - external - auth - { - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; - Metrics storage metrics_ = metrics[poolId]; - uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - - metrics_.issuance = metrics_.issuance + issuance - networkMetrics_.issuance; - metrics_.netAssetValue = metrics_.netAssetValue + netAssetValue - networkMetrics_.netAssetValue; - - D18 price = _navPerShare(poolId); - - networkMetrics_.netAssetValue = netAssetValue; - networkMetrics_.issuance = issuance; - - uint256 networkCount = metrics_.networks.length; - hub.updateSharePrice(poolId, scId, price); - - for (uint256 i; i < networkCount; i++) { - hub.notifySharePrice(poolId, scId, metrics_.networks[i]); - } - - emit Update(poolId, scId, metrics_.netAssetValue, metrics_.issuance, price); - } - - /// @inheritdoc INAVHook - function onTransfer( - PoolId poolId, - ShareClassId scId, - uint16 fromCentrifugeId, - uint16 toCentrifugeId, - uint128 sharesTransferred - ) external auth { - require(scId.index() == 1, InvalidShareClass()); - NetworkMetrics storage fromMetrics = networkMetrics[poolId][fromCentrifugeId]; - NetworkMetrics storage toMetrics = networkMetrics[poolId][toCentrifugeId]; - fromMetrics.issuance -= sharesTransferred; - toMetrics.issuance += sharesTransferred; - - emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); + super.onUpdate(poolId, scId, centrifugeId, netAssetValue); } //---------------------------------------------------------------------------------------------- // Manager actions //---------------------------------------------------------------------------------------------- + /// @inheritdoc ISimplePriceManager function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) external onlyManager(poolId) @@ -186,6 +64,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { ); } + /// @inheritdoc ISimplePriceManager function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external onlyManager(poolId) @@ -204,6 +83,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); } + /// @inheritdoc ISimplePriceManager function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) external onlyManager(poolId) @@ -223,6 +103,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { ); } + /// @inheritdoc ISimplePriceManager function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external onlyManager(poolId) @@ -288,13 +169,4 @@ contract SimplePriceManager is ISimplePriceManager, Auth { ); requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); } - - //---------------------------------------------------------------------------------------------- - // Helpers - //---------------------------------------------------------------------------------------------- - - function _navPerShare(PoolId poolId) internal view returns (D18) { - Metrics memory metrics_ = metrics[poolId]; - return metrics_.issuance == 0 ? d18(1, 1) : d18(metrics_.netAssetValue) / d18(metrics_.issuance); - } } diff --git a/src/managers/hub/SimplePriceManagerBase.sol b/src/managers/hub/SimplePriceManagerBase.sol new file mode 100644 index 000000000..8518bc179 --- /dev/null +++ b/src/managers/hub/SimplePriceManagerBase.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {INAVHook} from "./interfaces/INAVManager.sol"; +import {ISimplePriceManagerBase} from "./interfaces/ISimplePriceManagerBase.sol"; + +import {Auth} from "../../misc/Auth.sol"; +import {D18, d18} from "../../misc/types/D18.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; + +import {IHub} from "../../hub/interfaces/IHub.sol"; +import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; +import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; + +/// @notice Base share price calculation manager for single share class pools. +contract SimplePriceManagerBase is ISimplePriceManagerBase, Auth { + ICrosschainBatcher public crosschainBatcher; + IHub public immutable hub; + IHubRegistry public immutable hubRegistry; + IShareClassManager public immutable shareClassManager; + + mapping(PoolId poolId => Metrics) public metrics; + mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; + mapping(PoolId poolId => mapping(address => bool)) public manager; + + constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) Auth(deployer) { + hub = hub_; + crosschainBatcher = crosschainBatcher_; + hubRegistry = hub_.hubRegistry(); + shareClassManager = hub_.shareClassManager(); + } + + modifier onlyManager(PoolId poolId) { + require(manager[poolId][msg.sender], NotAuthorized()); + _; + } + + modifier onlyHubManager(PoolId poolId) { + require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); + _; + } + + //---------------------------------------------------------------------------------------------- + // Administration + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ISimplePriceManagerBase + function file(bytes32 what, address data) external auth { + if (what == "crosschainBatcher") crosschainBatcher = ICrosschainBatcher(data); + else revert ISimplePriceManagerBase.FileUnrecognizedParam(); + emit File(what, data); + } + + /// @inheritdoc ISimplePriceManagerBase + function networks(PoolId poolId) external view returns (uint16[] memory) { + return metrics[poolId].networks; + } + + /// @inheritdoc ISimplePriceManagerBase + function addNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { + require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); + + metrics[poolId].networks.push(centrifugeId); + emit UpdateNetworks(poolId, metrics[poolId].networks); + } + + /// @inheritdoc ISimplePriceManagerBase + function removeNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { + uint16[] storage networks_ = metrics[poolId].networks; + uint256 length = networks_.length; + for (uint256 i; i < length; i++) { + if (networks_[i] == centrifugeId) { + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; + Metrics storage metrics_ = metrics[poolId]; + + metrics_.netAssetValue -= networkMetrics_.netAssetValue; + metrics_.issuance -= networkMetrics_.issuance; + + delete networkMetrics[poolId][centrifugeId]; + + networks_[i] = networks_[length - 1]; + networks_.pop(); + + emit UpdateNetworks(poolId, networks_); + return; + } + } + revert NetworkNotFound(); + } + + /// @inheritdoc ISimplePriceManagerBase + function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { + manager[poolId][manager_] = canManage; + + emit UpdateManager(poolId, manager_, canManage); + } + + //---------------------------------------------------------------------------------------------- + // Updates + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc INAVHook + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) + public + virtual + auth + { + require(scId.index() == 1, InvalidShareClass()); + + crosschainBatcher.execute( + abi.encodeWithSelector( + SimplePriceManagerBase.onUpdateCallback.selector, poolId, scId, centrifugeId, netAssetValue + ) + ); + } + + function onUpdateCallback(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) + external + auth + { + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; + Metrics storage metrics_ = metrics[poolId]; + uint128 issuance = shareClassManager.issuance(scId, centrifugeId); + + metrics_.issuance = metrics_.issuance + issuance - networkMetrics_.issuance; + metrics_.netAssetValue = metrics_.netAssetValue + netAssetValue - networkMetrics_.netAssetValue; + + D18 price = _navPerShare(poolId); + + networkMetrics_.netAssetValue = netAssetValue; + networkMetrics_.issuance = issuance; + + uint256 networkCount = metrics_.networks.length; + hub.updateSharePrice(poolId, scId, price); + + for (uint256 i; i < networkCount; i++) { + hub.notifySharePrice(poolId, scId, metrics_.networks[i]); + } + + emit Update(poolId, scId, metrics_.netAssetValue, metrics_.issuance, price); + } + + /// @inheritdoc INAVHook + function onTransfer( + PoolId poolId, + ShareClassId scId, + uint16 fromCentrifugeId, + uint16 toCentrifugeId, + uint128 sharesTransferred + ) external auth { + require(scId.index() == 1, InvalidShareClass()); + NetworkMetrics storage fromMetrics = networkMetrics[poolId][fromCentrifugeId]; + NetworkMetrics storage toMetrics = networkMetrics[poolId][toCentrifugeId]; + fromMetrics.issuance -= sharesTransferred; + toMetrics.issuance += sharesTransferred; + + emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); + } + + //---------------------------------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------------------------------- + + function _navPerShare(PoolId poolId) internal view returns (D18) { + Metrics memory metrics_ = metrics[poolId]; + return metrics_.issuance == 0 ? d18(1, 1) : d18(metrics_.netAssetValue) / d18(metrics_.issuance); + } +} diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index ee4eaff7d..3b4fba816 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -1,76 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {INAVHook} from "./INAVManager.sol"; - -import {D18} from "../../../misc/types/D18.sol"; +import {ISimplePriceManagerBase} from "./ISimplePriceManagerBase.sol"; import {PoolId} from "../../../common/types/PoolId.sol"; import {AssetId} from "../../../common/types/AssetId.sol"; import {ShareClassId} from "../../../common/types/ShareClassId.sol"; -interface ISimplePriceManager is INAVHook { - event Update(PoolId indexed poolId, ShareClassId scId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); - event Transfer( - PoolId indexed poolId, - ShareClassId scId, - uint16 indexed fromCentrifugeId, - uint16 indexed toCentrifugeId, - uint128 sharesTransferred - ); - event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); - event UpdateNetworks(PoolId indexed poolId, uint16[] networks); - event File(bytes32 indexed what, address data); - - error InvalidShareClassCount(); - error InvalidShareClass(); - error MismatchedEpochs(); - error FileUnrecognizedParam(); - error NetworkNotFound(); - - struct Metrics { - uint128 netAssetValue; - uint128 issuance; - uint16[] networks; - } - - struct NetworkMetrics { - uint128 netAssetValue; - uint128 issuance; - uint32 issueEpochsBehind; - uint32 revokeEpochsBehind; - } - - function metrics(PoolId poolId) external view returns (uint128 netAssetValue, uint128 issuance); - function networks(PoolId poolId) external view returns (uint16[] memory networks); - function networkMetrics(PoolId poolId, uint16 centrifugeId) - external - view - returns (uint128 netAssetValue, uint128 issuance, uint32 issueEpochsBehind, uint32 revokeEpochsBehind); - function manager(PoolId poolId, address manager_) external view returns (bool); - - //---------------------------------------------------------------------------------------------- - // Administration - //---------------------------------------------------------------------------------------------- - - /// @notice Add a network to the pool - /// @param poolId The pool ID - /// @param centrifugeId Centrifuge ID for the network to add - function addNetwork(PoolId poolId, uint16 centrifugeId) external; - - /// @notice Remove a network from the pool - /// @param poolId The pool ID - /// @param centrifugeId Centrifuge ID for the network to remove - function removeNetwork(PoolId poolId, uint16 centrifugeId) external; - - /// @notice Update whether an address can manage the NAV manager - /// @param poolId The pool ID - /// @param manager The address of the manager - /// @param canManage Whether the address can manage this manager - function updateManager(PoolId poolId, address manager, bool canManage) external; - - function file(bytes32 what, address data) external; - +interface ISimplePriceManager is ISimplePriceManagerBase { //---------------------------------------------------------------------------------------------- // Manager actions //---------------------------------------------------------------------------------------------- @@ -83,7 +20,7 @@ interface ISimplePriceManager is INAVHook { function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) external; - /// @notice Issue shares from approved deposit requests + /// @notice Issue shares for approved deposit epochs /// @param poolId The pool ID /// @param scId The share class ID /// @param depositAssetId The asset ID for deposits diff --git a/src/managers/hub/interfaces/ISimplePriceManagerBase.sol b/src/managers/hub/interfaces/ISimplePriceManagerBase.sol new file mode 100644 index 000000000..b2b27f436 --- /dev/null +++ b/src/managers/hub/interfaces/ISimplePriceManagerBase.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {INAVHook} from "./INAVManager.sol"; + +import {D18} from "../../../misc/types/D18.sol"; + +import {PoolId} from "../../../common/types/PoolId.sol"; +import {AssetId} from "../../../common/types/AssetId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; + +interface ISimplePriceManagerBase is INAVHook { + event Update(PoolId indexed poolId, ShareClassId scId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); + event Transfer( + PoolId indexed poolId, + ShareClassId scId, + uint16 indexed fromCentrifugeId, + uint16 indexed toCentrifugeId, + uint128 sharesTransferred + ); + event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); + event UpdateNetworks(PoolId indexed poolId, uint16[] networks); + event File(bytes32 indexed what, address data); + + error InvalidShareClassCount(); + error InvalidShareClass(); + error MismatchedEpochs(); + error FileUnrecognizedParam(); + error NetworkNotFound(); + + struct Metrics { + uint128 netAssetValue; + uint128 issuance; + uint16[] networks; + } + + struct NetworkMetrics { + uint128 netAssetValue; + uint128 issuance; + uint32 issueEpochsBehind; + uint32 revokeEpochsBehind; + } + + function metrics(PoolId poolId) external view returns (uint128 netAssetValue, uint128 issuance); + function networks(PoolId poolId) external view returns (uint16[] memory networks); + function networkMetrics(PoolId poolId, uint16 centrifugeId) + external + view + returns (uint128 netAssetValue, uint128 issuance, uint32 issueEpochsBehind, uint32 revokeEpochsBehind); + function manager(PoolId poolId, address manager_) external view returns (bool); + + //---------------------------------------------------------------------------------------------- + // Administration + //---------------------------------------------------------------------------------------------- + + /// @notice Add a network to the pool + /// @param poolId The pool ID + /// @param centrifugeId Centrifuge ID for the network to add + function addNetwork(PoolId poolId, uint16 centrifugeId) external; + + /// @notice Remove a network from the pool + /// @param poolId The pool ID + /// @param centrifugeId Centrifuge ID for the network to remove + function removeNetwork(PoolId poolId, uint16 centrifugeId) external; + + /// @notice Update whether an address can manage the NAV manager + /// @param poolId The pool ID + /// @param manager The address of the manager + /// @param canManage Whether the address can manage this manager + function updateManager(PoolId poolId, address manager, bool canManage) external; + + function file(bytes32 what, address data) external; +} diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index fe79a5abc..cb90d001e 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -7,15 +7,16 @@ import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; import {PoolId} from "../../../../src/common/types/PoolId.sol"; import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; -import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; import {ICrosschainBatcher} from "../../../../src/common/interfaces/ICrosschainBatcher.sol"; +import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; import {SimplePriceManager} from "../../../../src/managers/hub/SimplePriceManager.sol"; import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; +import {ISimplePriceManagerBase} from "../../../../src/managers/hub/interfaces/ISimplePriceManagerBase.sol"; import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; @@ -190,7 +191,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { networks[0] = CENTRIFUGE_ID_1; vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.UpdateNetworks(POOL_A, networks); + emit ISimplePriceManagerBase.UpdateNetworks(POOL_A, networks); vm.prank(hubManager); priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); @@ -204,7 +205,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { networks2[1] = CENTRIFUGE_ID_2; vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.UpdateNetworks(POOL_A, networks2); + emit ISimplePriceManagerBase.UpdateNetworks(POOL_A, networks2); vm.prank(hubManager); priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_2); @@ -231,7 +232,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_B, hubManager), abi.encode(true) ); - vm.expectRevert(ISimplePriceManager.InvalidShareClassCount.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClassCount.selector); vm.prank(hubManager); priceManager.addNetwork(POOL_B, CENTRIFUGE_ID_1); } @@ -290,7 +291,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { vm.prank(hubManager); priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); - vm.expectRevert(ISimplePriceManager.NetworkNotFound.selector); + vm.expectRevert(ISimplePriceManagerBase.NetworkNotFound.selector); vm.prank(hubManager); priceManager.removeNetwork(POOL_A, CENTRIFUGE_ID_2); } @@ -299,7 +300,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { address newManager = makeAddr("newManager"); vm.expectEmit(true, true, false, false); - emit ISimplePriceManager.UpdateManager(POOL_A, newManager, true); + emit ISimplePriceManagerBase.UpdateManager(POOL_A, newManager, true); vm.prank(hubManager); priceManager.updateManager(POOL_A, newManager, true); @@ -315,7 +316,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { assertTrue(priceManager.manager(POOL_A, managerAddr)); vm.expectEmit(true, true, false, false); - emit ISimplePriceManager.UpdateManager(POOL_A, managerAddr, false); + emit ISimplePriceManagerBase.UpdateManager(POOL_A, managerAddr, false); vm.prank(hubManager); priceManager.updateManager(POOL_A, managerAddr, false); @@ -337,7 +338,7 @@ contract SimplePriceManagerFileTests is SimplePriceManagerTest { address newCrosschainBatcher = makeAddr("newCrosschainBatcher"); vm.expectEmit(true, false, true, true); - emit ISimplePriceManager.File("crosschainBatcher", newCrosschainBatcher); + emit ISimplePriceManagerBase.File("crosschainBatcher", newCrosschainBatcher); vm.prank(auth); priceManager.file("crosschainBatcher", newCrosschainBatcher); @@ -348,7 +349,7 @@ contract SimplePriceManagerFileTests is SimplePriceManagerTest { function testFileUnrecognizedParam() public { address someAddress = makeAddr("someAddress"); - vm.expectRevert(ISimplePriceManager.FileUnrecognizedParam.selector); + vm.expectRevert(ISimplePriceManagerBase.FileUnrecognizedParam.selector); vm.prank(auth); priceManager.file("invalid", someAddress); } @@ -387,7 +388,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { ); vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(POOL_A, SC_1, netAssetValue, 100, d18(10, 1)); + emit ISimplePriceManagerBase.Update(POOL_A, SC_1, netAssetValue, 100, d18(10, 1)); vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, netAssetValue); @@ -411,7 +412,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateSharePrice.selector, POOL_A, SC_1, d18(9, 1))); vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(POOL_A, SC_1, 2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 + emit ISimplePriceManagerBase.Update(POOL_A, SC_1, 2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, netAssetValue2); @@ -434,7 +435,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { uint128 newNetAssetValue = 1200; vm.expectEmit(true, true, true, true); - emit ISimplePriceManager.Update(POOL_A, SC_1, 1200, 150, d18(8, 1)); // 1200/150 = 8 + emit ISimplePriceManagerBase.Update(POOL_A, SC_1, 1200, 150, d18(8, 1)); // 1200/150 = 8 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, newNetAssetValue); @@ -468,7 +469,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { } function testInvalidShareClass() public { - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(caller); priceManager.onUpdate(POOL_A, SC_2, CENTRIFUGE_ID_1, 1000); } @@ -489,7 +490,7 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { uint128 sharesTransferred = 50; vm.expectEmit(true, true, false, true); - emit ISimplePriceManager.Transfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); + emit ISimplePriceManagerBase.Transfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); @@ -523,7 +524,7 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { } function testInvalidShareClass() public { - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(caller); priceManager.onTransfer(POOL_A, SC_2, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); } @@ -584,7 +585,7 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { abi.encode(2) ); - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); vm.prank(manager); priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); } @@ -640,7 +641,7 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { abi.encode(3) ); - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); vm.prank(manager); priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); } @@ -703,7 +704,7 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { } function testRevokeSharesWithoutPendingEpochs() public { - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); vm.prank(manager); priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); } @@ -760,33 +761,33 @@ contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { } function testIssueSharesWithoutPendingEpochs() public { - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); vm.prank(manager); priceManager.issueShares(POOL_A, SC_1, asset1, 100000); } function testInvalidShareClass() public { - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(manager); priceManager.approveDeposits(POOL_A, SC_2, asset1, 1); - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(manager); priceManager.issueShares(POOL_A, SC_2, asset1, 1); - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(manager); priceManager.approveRedeems(POOL_A, SC_2, asset1, 1); - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(manager); priceManager.revokeShares(POOL_A, SC_2, asset1, 1); - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(manager); priceManager.approveDepositsAndIssueShares(POOL_A, SC_2, asset1, 1, 1); - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); vm.prank(manager); priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_2, asset1, 1, 1); } From fd81467ac005a1e7f007f10ea6d39d253c6e3e74 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:44:47 +0200 Subject: [PATCH 71/83] remove setHoldingAccountId --- src/managers/hub/NAVManager.sol | 9 --------- src/managers/hub/interfaces/INAVManager.sol | 9 --------- test/managers/hub/unit/NAVManager.t.sol | 22 --------------------- 3 files changed, 40 deletions(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index 1a123c899..70b7f6a3b 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -163,15 +163,6 @@ contract NAVManager is INAVManager, Auth { hub.updateHoldingValue(poolId, scId, assetId); } - /// @inheritdoc INAVManager - function setHoldingAccountId(PoolId poolId, ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) - external - onlyManager(poolId) - { - // TODO: Should we have this function at all? Seems like this can only mess up the accounting. - hub.setHoldingAccountId(poolId, scId, assetId, kind, accountId); - } - /// @inheritdoc INAVManager function closeGainLoss(PoolId poolId, uint16 centrifugeId) external onlyManager(poolId) { require(initialized[poolId][centrifugeId], NotInitialized()); diff --git a/src/managers/hub/interfaces/INAVManager.sol b/src/managers/hub/interfaces/INAVManager.sol index bc36cf1cc..5d6ab488d 100644 --- a/src/managers/hub/interfaces/INAVManager.sol +++ b/src/managers/hub/interfaces/INAVManager.sol @@ -122,15 +122,6 @@ interface INAVManager is ISnapshotHook { /// @param valuation The new valuation contract function updateHoldingValuation(PoolId poolId, ShareClassId scId, AssetId assetId, IValuation valuation) external; - /// @notice Set the account ID for a specific asset holding - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param assetId The asset ID - /// @param kind The account kind - /// @param accountId The account ID to set - function setHoldingAccountId(PoolId poolId, ShareClassId scId, AssetId assetId, uint8 kind, AccountId accountId) - external; - /// @notice close gain/loss accounts by moving balances to equity account /// @param poolId The pool ID /// @param centrifugeId The Centrifuge ID of the network diff --git a/test/managers/hub/unit/NAVManager.t.sol b/test/managers/hub/unit/NAVManager.t.sol index c3d6b7eee..2dd68d587 100644 --- a/test/managers/hub/unit/NAVManager.t.sol +++ b/test/managers/hub/unit/NAVManager.t.sol @@ -64,7 +64,6 @@ contract NAVManagerTest is Test { vm.mockCall(hub, abi.encodeWithSelector(IHub.initializeLiability.selector), abi.encode()); vm.mockCall(hub, abi.encodeWithSelector(IHub.updateHoldingValue.selector), abi.encode()); vm.mockCall(hub, abi.encodeWithSelector(IHub.updateHoldingValuation.selector), abi.encode()); - vm.mockCall(hub, abi.encodeWithSelector(IHub.setHoldingAccountId.selector), abi.encode()); vm.mockCall(holdings, abi.encodeWithSelector(IHoldings.snapshot.selector), abi.encode(false, uint64(0))); @@ -442,27 +441,6 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { navManager.updateHoldingValuation(POOL_A, SC_1, asset1, mockValuation); } - function testSetHoldingAccountId() public { - AccountId accountId = withCentrifugeId(CENTRIFUGE_ID_1, 10); - uint8 kind = 1; - - vm.expectCall( - address(hub), - abi.encodeWithSelector(IHub.setHoldingAccountId.selector, POOL_A, SC_1, asset1, kind, accountId) - ); - - vm.prank(manager); - navManager.setHoldingAccountId(POOL_A, SC_1, asset1, kind, accountId); - } - - function testSetHoldingAccountIdUnauthorized() public { - AccountId accountId = withCentrifugeId(CENTRIFUGE_ID_1, 10); - uint8 kind = 1; - - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - navManager.setHoldingAccountId(POOL_A, SC_1, asset1, kind, accountId); - } } contract NAVManagerCloseGainLossTest is NAVManagerTest { From 6eab680293fa35d2da53e0d2204af1074c2edaa7 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:45:03 +0200 Subject: [PATCH 72/83] make updateHoldingValue permissionless --- src/managers/hub/NAVManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index 70b7f6a3b..967fe937f 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -150,7 +150,7 @@ contract NAVManager is INAVManager, Auth { //---------------------------------------------------------------------------------------------- /// @inheritdoc INAVManager - function updateHoldingValue(PoolId poolId, ShareClassId scId, AssetId assetId) external onlyManager(poolId) { + function updateHoldingValue(PoolId poolId, ShareClassId scId, AssetId assetId) external { hub.updateHoldingValue(poolId, scId, assetId); } From 3d8ab8c96037c98960a81a11170f74b92e9c054f Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:12:49 +0200 Subject: [PATCH 73/83] rename --- src/managers/hub/BatchSimplePriceManager.sol | 172 ++++++++ src/managers/hub/SimplePriceManager.sol | 234 +++++----- src/managers/hub/SimplePriceManagerBase.sol | 172 -------- .../interfaces/IBatchSimplePriceManager.sol | 72 +++ .../hub/interfaces/ISimplePriceManager.sol | 109 ++--- .../interfaces/ISimplePriceManagerBase.sol | 73 ---- .../hub/unit/BatchSimplePriceManager.t.sol | 413 ++++++++++++++++++ test/managers/hub/unit/NAVManager.t.sol | 7 - .../hub/unit/SimplePriceManager.t.sol | 342 +-------------- 9 files changed, 843 insertions(+), 751 deletions(-) create mode 100644 src/managers/hub/BatchSimplePriceManager.sol delete mode 100644 src/managers/hub/SimplePriceManagerBase.sol create mode 100644 src/managers/hub/interfaces/IBatchSimplePriceManager.sol delete mode 100644 src/managers/hub/interfaces/ISimplePriceManagerBase.sol create mode 100644 test/managers/hub/unit/BatchSimplePriceManager.t.sol diff --git a/src/managers/hub/BatchSimplePriceManager.sol b/src/managers/hub/BatchSimplePriceManager.sol new file mode 100644 index 000000000..a482132ee --- /dev/null +++ b/src/managers/hub/BatchSimplePriceManager.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {INAVHook} from "./interfaces/INAVManager.sol"; +import {SimplePriceManager} from "./SimplePriceManager.sol"; +import {IBatchSimplePriceManager} from "./interfaces/IBatchSimplePriceManager.sol"; + +import {D18, d18} from "../../misc/types/D18.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; + +import {IHub} from "../../hub/interfaces/IHub.sol"; + +import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; + +/// @notice Simple price manager for single share class pools with async request management. +contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager { + constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) + SimplePriceManager(hub_, crosschainBatcher_, deployer) + {} + + //---------------------------------------------------------------------------------------------- + // Updates + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc SimplePriceManager + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) + public + override(SimplePriceManager, INAVHook) + auth + { + NetworkMetrics memory networkMetrics_ = networkMetrics[poolId][centrifugeId]; + + // If there are pending epochs to be issued or revoked, skip updating the share price, as it will likely be off + if (networkMetrics_.issueEpochsBehind > 0 || networkMetrics_.revokeEpochsBehind > 0) return; + + super.onUpdate(poolId, scId, centrifugeId, netAssetValue); + } + + //---------------------------------------------------------------------------------------------- + // Manager actions + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc IBatchSimplePriceManager + function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) + external + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + + networkMetrics_.issueEpochsBehind++; + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); + requestManager.approveDeposits( + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset + ); + } + + /// @inheritdoc IBatchSimplePriceManager + function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) + external + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + + require(networkMetrics_.issueEpochsBehind > 0, MismatchedEpochs()); + networkMetrics_.issueEpochsBehind--; + + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + } + + /// @inheritdoc IBatchSimplePriceManager + function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) + external + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; + + networkMetrics_.revokeEpochsBehind++; + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); + requestManager.approveRedeems( + poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset + ); + } + + /// @inheritdoc IBatchSimplePriceManager + function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) + external + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; + + require(networkMetrics_.revokeEpochsBehind > 0, MismatchedEpochs()); + networkMetrics_.revokeEpochsBehind--; + + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + } + + /// @inheritdoc IBatchSimplePriceManager + function approveDepositsAndIssueShares( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); + uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); + + require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.approveDeposits( + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset + ); + requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + } + + /// @inheritdoc IBatchSimplePriceManager + function approveRedeemsAndRevokeShares( + PoolId poolId, + ShareClassId scId, + AssetId payoutAssetId, + uint128 approvedShareAmount, + uint128 extraGasLimit + ) external onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); + uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); + + require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.approveRedeems( + poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset + ); + requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + } +} diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 96a383812..9009d10e0 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.28; import {INAVHook} from "./interfaces/INAVManager.sol"; -import {SimplePriceManagerBase} from "./SimplePriceManagerBase.sol"; import {ISimplePriceManager} from "./interfaces/ISimplePriceManager.sol"; +import {Auth} from "../../misc/Auth.sol"; import {D18, d18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; @@ -13,160 +13,160 @@ import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; import {IHub} from "../../hub/interfaces/IHub.sol"; +import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; +import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; + +/// @notice Base share price calculation manager for single share class pools. +contract SimplePriceManager is ISimplePriceManager, Auth { + ICrosschainBatcher public crosschainBatcher; + IHub public immutable hub; + IHubRegistry public immutable hubRegistry; + IShareClassManager public immutable shareClassManager; + + mapping(PoolId poolId => Metrics) public metrics; + mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; + mapping(PoolId poolId => mapping(address => bool)) public manager; + + constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) Auth(deployer) { + hub = hub_; + crosschainBatcher = crosschainBatcher_; + hubRegistry = hub_.hubRegistry(); + shareClassManager = hub_.shareClassManager(); + } -import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; - -/// @notice Simple price manager for single share class pools with async request management. -contract SimplePriceManager is SimplePriceManagerBase, ISimplePriceManager { - constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) - SimplePriceManagerBase(hub_, crosschainBatcher_, deployer) - {} - - //---------------------------------------------------------------------------------------------- - // Updates - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc SimplePriceManagerBase - function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) - public - override(SimplePriceManagerBase, INAVHook) - auth - { - NetworkMetrics memory networkMetrics_ = networkMetrics[poolId][centrifugeId]; - - // If there are pending epochs to be issued or revoked, skip updating the share price, as it will likely be off - if (networkMetrics_.issueEpochsBehind > 0 || networkMetrics_.revokeEpochsBehind > 0) return; + modifier onlyManager(PoolId poolId) { + require(manager[poolId][msg.sender], NotAuthorized()); + _; + } - super.onUpdate(poolId, scId, centrifugeId, netAssetValue); + modifier onlyHubManager(PoolId poolId) { + require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); + _; } //---------------------------------------------------------------------------------------------- - // Manager actions + // Administration //---------------------------------------------------------------------------------------------- /// @inheritdoc ISimplePriceManager - function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) - external - onlyManager(poolId) - { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); - uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); + function file(bytes32 what, address data) external auth { + if (what == "crosschainBatcher") crosschainBatcher = ICrosschainBatcher(data); + else revert ISimplePriceManager.FileUnrecognizedParam(); + emit File(what, data); + } - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + /// @inheritdoc ISimplePriceManager + function networks(PoolId poolId) external view returns (uint16[] memory) { + return metrics[poolId].networks; + } - networkMetrics_.issueEpochsBehind++; + /// @inheritdoc ISimplePriceManager + function addNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { + require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); - requestManager.approveDeposits( - poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset - ); + metrics[poolId].networks.push(centrifugeId); + emit UpdateNetworks(poolId, metrics[poolId].networks); } /// @inheritdoc ISimplePriceManager - function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) - external - onlyManager(poolId) - { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); - uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); - - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + function removeNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { + uint16[] storage networks_ = metrics[poolId].networks; + uint256 length = networks_.length; + for (uint256 i; i < length; i++) { + if (networks_[i] == centrifugeId) { + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; + Metrics storage metrics_ = metrics[poolId]; + + metrics_.netAssetValue -= networkMetrics_.netAssetValue; + metrics_.issuance -= networkMetrics_.issuance; + + delete networkMetrics[poolId][centrifugeId]; + + networks_[i] = networks_[length - 1]; + networks_.pop(); + + emit UpdateNetworks(poolId, networks_); + return; + } + } + revert NetworkNotFound(); + } - require(networkMetrics_.issueEpochsBehind > 0, MismatchedEpochs()); - networkMetrics_.issueEpochsBehind--; + /// @inheritdoc ISimplePriceManager + function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { + manager[poolId][manager_] = canManage; - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + emit UpdateManager(poolId, manager_, canManage); } - /// @inheritdoc ISimplePriceManager - function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) - external - onlyManager(poolId) + //---------------------------------------------------------------------------------------------- + // Updates + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc INAVHook + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) + public + virtual + auth { require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); - uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); - - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; - networkMetrics_.revokeEpochsBehind++; - - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); - requestManager.approveRedeems( - poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset + crosschainBatcher.execute( + abi.encodeWithSelector( + SimplePriceManager.onUpdateCallback.selector, poolId, scId, centrifugeId, netAssetValue + ) ); } - /// @inheritdoc ISimplePriceManager - function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) + function onUpdateCallback(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external - onlyManager(poolId) + auth { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); - uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; + Metrics storage metrics_ = metrics[poolId]; + uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; + metrics_.issuance = metrics_.issuance + issuance - networkMetrics_.issuance; + metrics_.netAssetValue = metrics_.netAssetValue + netAssetValue - networkMetrics_.netAssetValue; - require(networkMetrics_.revokeEpochsBehind > 0, MismatchedEpochs()); - networkMetrics_.revokeEpochsBehind--; + D18 price = _navPerShare(poolId); - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); - } + networkMetrics_.netAssetValue = netAssetValue; + networkMetrics_.issuance = issuance; - /// @inheritdoc ISimplePriceManager - function approveDepositsAndIssueShares( - PoolId poolId, - ShareClassId scId, - AssetId depositAssetId, - uint128 approvedAssetAmount, - uint128 extraGasLimit - ) external onlyManager(poolId) { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); - uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); - uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); + uint256 networkCount = metrics_.networks.length; + hub.updateSharePrice(poolId, scId, price); - require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); + for (uint256 i; i < networkCount; i++) { + hub.notifySharePrice(poolId, scId, metrics_.networks[i]); + } - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.approveDeposits( - poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset - ); - requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + emit Update(poolId, scId, metrics_.netAssetValue, metrics_.issuance, price); } - /// @inheritdoc ISimplePriceManager - function approveRedeemsAndRevokeShares( + /// @inheritdoc INAVHook + function onTransfer( PoolId poolId, ShareClassId scId, - AssetId payoutAssetId, - uint128 approvedShareAmount, - uint128 extraGasLimit - ) external onlyManager(poolId) { + uint16 fromCentrifugeId, + uint16 toCentrifugeId, + uint128 sharesTransferred + ) external auth { require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); - uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); - uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); + NetworkMetrics storage fromMetrics = networkMetrics[poolId][fromCentrifugeId]; + NetworkMetrics storage toMetrics = networkMetrics[poolId][toCentrifugeId]; + fromMetrics.issuance -= sharesTransferred; + toMetrics.issuance += sharesTransferred; - require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); + emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); + } - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.approveRedeems( - poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset - ); - requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + //---------------------------------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------------------------------- + + function _navPerShare(PoolId poolId) internal view returns (D18) { + Metrics memory metrics_ = metrics[poolId]; + return metrics_.issuance == 0 ? d18(1, 1) : d18(metrics_.netAssetValue) / d18(metrics_.issuance); } } diff --git a/src/managers/hub/SimplePriceManagerBase.sol b/src/managers/hub/SimplePriceManagerBase.sol deleted file mode 100644 index 8518bc179..000000000 --- a/src/managers/hub/SimplePriceManagerBase.sol +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {INAVHook} from "./interfaces/INAVManager.sol"; -import {ISimplePriceManagerBase} from "./interfaces/ISimplePriceManagerBase.sol"; - -import {Auth} from "../../misc/Auth.sol"; -import {D18, d18} from "../../misc/types/D18.sol"; - -import {PoolId} from "../../common/types/PoolId.sol"; -import {AssetId} from "../../common/types/AssetId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; - -import {IHub} from "../../hub/interfaces/IHub.sol"; -import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; -import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; - -/// @notice Base share price calculation manager for single share class pools. -contract SimplePriceManagerBase is ISimplePriceManagerBase, Auth { - ICrosschainBatcher public crosschainBatcher; - IHub public immutable hub; - IHubRegistry public immutable hubRegistry; - IShareClassManager public immutable shareClassManager; - - mapping(PoolId poolId => Metrics) public metrics; - mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; - mapping(PoolId poolId => mapping(address => bool)) public manager; - - constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) Auth(deployer) { - hub = hub_; - crosschainBatcher = crosschainBatcher_; - hubRegistry = hub_.hubRegistry(); - shareClassManager = hub_.shareClassManager(); - } - - modifier onlyManager(PoolId poolId) { - require(manager[poolId][msg.sender], NotAuthorized()); - _; - } - - modifier onlyHubManager(PoolId poolId) { - require(hubRegistry.manager(poolId, msg.sender), NotAuthorized()); - _; - } - - //---------------------------------------------------------------------------------------------- - // Administration - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc ISimplePriceManagerBase - function file(bytes32 what, address data) external auth { - if (what == "crosschainBatcher") crosschainBatcher = ICrosschainBatcher(data); - else revert ISimplePriceManagerBase.FileUnrecognizedParam(); - emit File(what, data); - } - - /// @inheritdoc ISimplePriceManagerBase - function networks(PoolId poolId) external view returns (uint16[] memory) { - return metrics[poolId].networks; - } - - /// @inheritdoc ISimplePriceManagerBase - function addNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { - require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); - - metrics[poolId].networks.push(centrifugeId); - emit UpdateNetworks(poolId, metrics[poolId].networks); - } - - /// @inheritdoc ISimplePriceManagerBase - function removeNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { - uint16[] storage networks_ = metrics[poolId].networks; - uint256 length = networks_.length; - for (uint256 i; i < length; i++) { - if (networks_[i] == centrifugeId) { - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; - Metrics storage metrics_ = metrics[poolId]; - - metrics_.netAssetValue -= networkMetrics_.netAssetValue; - metrics_.issuance -= networkMetrics_.issuance; - - delete networkMetrics[poolId][centrifugeId]; - - networks_[i] = networks_[length - 1]; - networks_.pop(); - - emit UpdateNetworks(poolId, networks_); - return; - } - } - revert NetworkNotFound(); - } - - /// @inheritdoc ISimplePriceManagerBase - function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { - manager[poolId][manager_] = canManage; - - emit UpdateManager(poolId, manager_, canManage); - } - - //---------------------------------------------------------------------------------------------- - // Updates - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc INAVHook - function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) - public - virtual - auth - { - require(scId.index() == 1, InvalidShareClass()); - - crosschainBatcher.execute( - abi.encodeWithSelector( - SimplePriceManagerBase.onUpdateCallback.selector, poolId, scId, centrifugeId, netAssetValue - ) - ); - } - - function onUpdateCallback(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) - external - auth - { - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; - Metrics storage metrics_ = metrics[poolId]; - uint128 issuance = shareClassManager.issuance(scId, centrifugeId); - - metrics_.issuance = metrics_.issuance + issuance - networkMetrics_.issuance; - metrics_.netAssetValue = metrics_.netAssetValue + netAssetValue - networkMetrics_.netAssetValue; - - D18 price = _navPerShare(poolId); - - networkMetrics_.netAssetValue = netAssetValue; - networkMetrics_.issuance = issuance; - - uint256 networkCount = metrics_.networks.length; - hub.updateSharePrice(poolId, scId, price); - - for (uint256 i; i < networkCount; i++) { - hub.notifySharePrice(poolId, scId, metrics_.networks[i]); - } - - emit Update(poolId, scId, metrics_.netAssetValue, metrics_.issuance, price); - } - - /// @inheritdoc INAVHook - function onTransfer( - PoolId poolId, - ShareClassId scId, - uint16 fromCentrifugeId, - uint16 toCentrifugeId, - uint128 sharesTransferred - ) external auth { - require(scId.index() == 1, InvalidShareClass()); - NetworkMetrics storage fromMetrics = networkMetrics[poolId][fromCentrifugeId]; - NetworkMetrics storage toMetrics = networkMetrics[poolId][toCentrifugeId]; - fromMetrics.issuance -= sharesTransferred; - toMetrics.issuance += sharesTransferred; - - emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); - } - - //---------------------------------------------------------------------------------------------- - // Helpers - //---------------------------------------------------------------------------------------------- - - function _navPerShare(PoolId poolId) internal view returns (D18) { - Metrics memory metrics_ = metrics[poolId]; - return metrics_.issuance == 0 ? d18(1, 1) : d18(metrics_.netAssetValue) / d18(metrics_.issuance); - } -} diff --git a/src/managers/hub/interfaces/IBatchSimplePriceManager.sol b/src/managers/hub/interfaces/IBatchSimplePriceManager.sol new file mode 100644 index 000000000..93a285711 --- /dev/null +++ b/src/managers/hub/interfaces/IBatchSimplePriceManager.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {ISimplePriceManager} from "./ISimplePriceManager.sol"; + +import {PoolId} from "../../../common/types/PoolId.sol"; +import {AssetId} from "../../../common/types/AssetId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; + +interface IBatchSimplePriceManager is ISimplePriceManager { + //---------------------------------------------------------------------------------------------- + // Manager actions + //---------------------------------------------------------------------------------------------- + + /// @notice Approve deposit requests for a given asset amount + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param approvedAssetAmount Amount of assets to approve for deposit + function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) + external; + + /// @notice Issue shares for approved deposit epochs + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external; + + /// @notice Approve redemption requests for a given share amount + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param approvedShareAmount Amount of shares to approve for redemption + function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) + external; + + /// @notice Revoke shares from approved redemption requests + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external; + + /// @notice Approve deposits and issue shares in sequence using current NAV per share + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param approvedAssetAmount Amount of assets to approve + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function approveDepositsAndIssueShares( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external; + + /// @notice Approve redeems and revoke shares in sequence using current NAV per share + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param approvedShareAmount Amount of shares to approve for redemption + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function approveRedeemsAndRevokeShares( + PoolId poolId, + ShareClassId scId, + AssetId payoutAssetId, + uint128 approvedShareAmount, + uint128 extraGasLimit + ) external; +} diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index 3b4fba816..9d5a1319e 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -1,72 +1,73 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {ISimplePriceManagerBase} from "./ISimplePriceManagerBase.sol"; +import {INAVHook} from "./INAVManager.sol"; + +import {D18} from "../../../misc/types/D18.sol"; import {PoolId} from "../../../common/types/PoolId.sol"; import {AssetId} from "../../../common/types/AssetId.sol"; import {ShareClassId} from "../../../common/types/ShareClassId.sol"; -interface ISimplePriceManager is ISimplePriceManagerBase { - //---------------------------------------------------------------------------------------------- - // Manager actions - //---------------------------------------------------------------------------------------------- +interface ISimplePriceManager is INAVHook { + event Update(PoolId indexed poolId, ShareClassId scId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); + event Transfer( + PoolId indexed poolId, + ShareClassId scId, + uint16 indexed fromCentrifugeId, + uint16 indexed toCentrifugeId, + uint128 sharesTransferred + ); + event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); + event UpdateNetworks(PoolId indexed poolId, uint16[] networks); + event File(bytes32 indexed what, address data); - /// @notice Approve deposit requests for a given asset amount - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param depositAssetId The asset ID for deposits - /// @param approvedAssetAmount Amount of assets to approve for deposit - function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) - external; + error InvalidShareClassCount(); + error InvalidShareClass(); + error MismatchedEpochs(); + error FileUnrecognizedParam(); + error NetworkNotFound(); - /// @notice Issue shares for approved deposit epochs - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param depositAssetId The asset ID for deposits - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external; + struct Metrics { + uint128 netAssetValue; + uint128 issuance; + uint16[] networks; + } - /// @notice Approve redemption requests for a given share amount - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param payoutAssetId The asset ID for payouts - /// @param approvedShareAmount Amount of shares to approve for redemption - function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) - external; + struct NetworkMetrics { + uint128 netAssetValue; + uint128 issuance; + uint32 issueEpochsBehind; + uint32 revokeEpochsBehind; + } - /// @notice Revoke shares from approved redemption requests + function metrics(PoolId poolId) external view returns (uint128 netAssetValue, uint128 issuance); + function networks(PoolId poolId) external view returns (uint16[] memory networks); + function networkMetrics(PoolId poolId, uint16 centrifugeId) + external + view + returns (uint128 netAssetValue, uint128 issuance, uint32 issueEpochsBehind, uint32 revokeEpochsBehind); + function manager(PoolId poolId, address manager_) external view returns (bool); + + //---------------------------------------------------------------------------------------------- + // Administration + //---------------------------------------------------------------------------------------------- + + /// @notice Add a network to the pool /// @param poolId The pool ID - /// @param scId The share class ID - /// @param payoutAssetId The asset ID for payouts - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external; + /// @param centrifugeId Centrifuge ID for the network to add + function addNetwork(PoolId poolId, uint16 centrifugeId) external; - /// @notice Approve deposits and issue shares in sequence using current NAV per share + /// @notice Remove a network from the pool /// @param poolId The pool ID - /// @param scId The share class ID - /// @param depositAssetId The asset ID for deposits - /// @param approvedAssetAmount Amount of assets to approve - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function approveDepositsAndIssueShares( - PoolId poolId, - ShareClassId scId, - AssetId depositAssetId, - uint128 approvedAssetAmount, - uint128 extraGasLimit - ) external; + /// @param centrifugeId Centrifuge ID for the network to remove + function removeNetwork(PoolId poolId, uint16 centrifugeId) external; - /// @notice Approve redeems and revoke shares in sequence using current NAV per share + /// @notice Update whether an address can manage the NAV manager /// @param poolId The pool ID - /// @param scId The share class ID - /// @param payoutAssetId The asset ID for payouts - /// @param approvedShareAmount Amount of shares to approve for redemption - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function approveRedeemsAndRevokeShares( - PoolId poolId, - ShareClassId scId, - AssetId payoutAssetId, - uint128 approvedShareAmount, - uint128 extraGasLimit - ) external; + /// @param manager The address of the manager + /// @param canManage Whether the address can manage this manager + function updateManager(PoolId poolId, address manager, bool canManage) external; + + function file(bytes32 what, address data) external; } diff --git a/src/managers/hub/interfaces/ISimplePriceManagerBase.sol b/src/managers/hub/interfaces/ISimplePriceManagerBase.sol deleted file mode 100644 index b2b27f436..000000000 --- a/src/managers/hub/interfaces/ISimplePriceManagerBase.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {INAVHook} from "./INAVManager.sol"; - -import {D18} from "../../../misc/types/D18.sol"; - -import {PoolId} from "../../../common/types/PoolId.sol"; -import {AssetId} from "../../../common/types/AssetId.sol"; -import {ShareClassId} from "../../../common/types/ShareClassId.sol"; - -interface ISimplePriceManagerBase is INAVHook { - event Update(PoolId indexed poolId, ShareClassId scId, uint128 newNAV, uint128 newIssuance, D18 newSharePrice); - event Transfer( - PoolId indexed poolId, - ShareClassId scId, - uint16 indexed fromCentrifugeId, - uint16 indexed toCentrifugeId, - uint128 sharesTransferred - ); - event UpdateManager(PoolId indexed poolId, address indexed manager, bool canManage); - event UpdateNetworks(PoolId indexed poolId, uint16[] networks); - event File(bytes32 indexed what, address data); - - error InvalidShareClassCount(); - error InvalidShareClass(); - error MismatchedEpochs(); - error FileUnrecognizedParam(); - error NetworkNotFound(); - - struct Metrics { - uint128 netAssetValue; - uint128 issuance; - uint16[] networks; - } - - struct NetworkMetrics { - uint128 netAssetValue; - uint128 issuance; - uint32 issueEpochsBehind; - uint32 revokeEpochsBehind; - } - - function metrics(PoolId poolId) external view returns (uint128 netAssetValue, uint128 issuance); - function networks(PoolId poolId) external view returns (uint16[] memory networks); - function networkMetrics(PoolId poolId, uint16 centrifugeId) - external - view - returns (uint128 netAssetValue, uint128 issuance, uint32 issueEpochsBehind, uint32 revokeEpochsBehind); - function manager(PoolId poolId, address manager_) external view returns (bool); - - //---------------------------------------------------------------------------------------------- - // Administration - //---------------------------------------------------------------------------------------------- - - /// @notice Add a network to the pool - /// @param poolId The pool ID - /// @param centrifugeId Centrifuge ID for the network to add - function addNetwork(PoolId poolId, uint16 centrifugeId) external; - - /// @notice Remove a network from the pool - /// @param poolId The pool ID - /// @param centrifugeId Centrifuge ID for the network to remove - function removeNetwork(PoolId poolId, uint16 centrifugeId) external; - - /// @notice Update whether an address can manage the NAV manager - /// @param poolId The pool ID - /// @param manager The address of the manager - /// @param canManage Whether the address can manage this manager - function updateManager(PoolId poolId, address manager, bool canManage) external; - - function file(bytes32 what, address data) external; -} diff --git a/test/managers/hub/unit/BatchSimplePriceManager.t.sol b/test/managers/hub/unit/BatchSimplePriceManager.t.sol new file mode 100644 index 000000000..3b5dc200a --- /dev/null +++ b/test/managers/hub/unit/BatchSimplePriceManager.t.sol @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {D18, d18} from "../../../../src/misc/types/D18.sol"; +import {Multicall} from "../../../../src/misc/Multicall.sol"; +import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; + +import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; +import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; +import {ICrosschainBatcher} from "../../../../src/common/interfaces/ICrosschainBatcher.sol"; +import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; + +import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; +import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; +import {BatchSimplePriceManager} from "../../../../src/managers/hub/BatchSimplePriceManager.sol"; +import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; +import {IBatchSimplePriceManager} from "../../../../src/managers/hub/interfaces/IBatchSimplePriceManager.sol"; +import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; + +import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; + +import "forge-std/Test.sol"; + +contract IsContract {} + +contract MockCrosschainBatcher { + function execute(bytes memory data) external payable returns (uint256 cost) { + (bool success, bytes memory returnData) = msg.sender.call{value: msg.value}(data); + if (!success) { + uint256 length = returnData.length; + require(length != 0, "Empty revert"); + + assembly ("memory-safe") { + revert(add(32, returnData), length) + } + } + return 0; + } +} + +contract MockHub is Multicall { + function notifySharePrice(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external payable {} +} + +contract BatchSimplePriceManagerTest is Test { + PoolId constant POOL_A = PoolId.wrap(1); + ShareClassId immutable SC_1 = newShareClassId(POOL_A, 1); + ShareClassId immutable SC_2 = newShareClassId(POOL_A, 2); + uint16 constant CENTRIFUGE_ID_1 = 1; + + AssetId asset1 = newAssetId(1, 1); + + address hub = address(new MockHub()); + address hubRegistry = address(new IsContract()); + address shareClassManager = address(new IsContract()); + address batchRequestManager = address(new IsContract()); + address crosschainBatcher = address(new MockCrosschainBatcher()); + + address unauthorized = makeAddr("unauthorized"); + address hubManager = makeAddr("hubManager"); + address manager = makeAddr("manager"); + address caller = makeAddr("caller"); + address auth = makeAddr("auth"); + + BatchSimplePriceManager priceManager; + + function setUp() public virtual { + _setupMocks(); + _deployManager(); + } + + function _setupMocks() internal { + vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); + vm.mockCall( + hub, abi.encodeWithSelector(IHub.pricePoolPerAsset.selector, POOL_A, SC_1, asset1), abi.encode(d18(1, 1)) + ); + + vm.mockCall( + hubRegistry, + abi.encodeWithSelector(IHubRegistry.hubRequestManager.selector), + abi.encode(batchRequestManager) + ); + vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); + vm.mockCall( + hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_A, hubManager), abi.encode(true) + ); + + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_1), + abi.encode(100) + ); + + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.approveDeposits.selector), + abi.encode(uint256(0)) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.issueShares.selector), + abi.encode(uint256(0)) + ); + vm.mockCall( + batchRequestManager, abi.encodeWithSelector(IBatchRequestManager.approveRedeems.selector), abi.encode() + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.revokeShares.selector), + abi.encode(uint256(0)) + ); + } + + function _deployManager() internal { + priceManager = new BatchSimplePriceManager(IHub(hub), ICrosschainBatcher(crosschainBatcher), auth); + vm.prank(auth); + priceManager.rely(caller); + vm.prank(auth); + priceManager.rely(crosschainBatcher); + + vm.prank(hubManager); + priceManager.updateManager(POOL_A, manager, true); + + vm.deal(address(priceManager), 1 ether); + } +} + +contract BatchSimplePriceManagerInvestorActionsTest is BatchSimplePriceManagerTest { + D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 + + function setUp() public override { + super.setUp(); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + } + + function testApproveDepositsAndIssueSharesSuccess() public { + uint128 approvedAssetAmount = 500; + uint128 extraGasLimit = 100000; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveDeposits.selector, POOL_A, SC_1, asset1, 1, approvedAssetAmount, d18(1, 1) + ) + ); + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.issueShares.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + expectedNavPerShare, + extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, approvedAssetAmount, extraGasLimit); + } + + function testApproveDepositsAndIssueSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); + } + + function testApproveDepositsAndIssueSharesMismatchedEpochs() public { + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); + } + + function testApproveRedeemsAndRevokeSharesSuccess() public { + uint128 approvedShareAmount = 50; + uint128 extraGasLimit = 100000; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveRedeems.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + approvedShareAmount, + d18(1, 1) + ) + ); + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.revokeShares.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + expectedNavPerShare, + extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, approvedShareAmount, extraGasLimit); + } + + function testApproveRedeemsAndRevokeSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); + } + + function testApproveRedeemsAndRevokeSharesMismatchedEpochs() public { + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), + abi.encode(3) + ); + + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); + } + + function testApproveRedeemsSuccess() public { + uint128 approvedShareAmount = 50; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveRedeems.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + approvedShareAmount, + d18(1, 1) + ) + ); + + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_1, asset1, approvedShareAmount); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(revokeEpochsBehind, 1); + assertEq(issueEpochsBehind, 0); + } + + function testApproveRedeemsUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); + } + + function testRevokeSharesSuccess() public { + uint128 extraGasLimit = 100000; + + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_1, asset1, extraGasLimit); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(revokeEpochsBehind, 0); + assertEq(issueEpochsBehind, 0); + } + + function testRevokeSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); + } + + function testRevokeSharesWithoutPendingEpochs() public { + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); + } + + function testApproveDepositsSuccess() public { + uint128 approvedAssetAmount = 500; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveDeposits.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + approvedAssetAmount, + d18(1, 1) + ) + ); + + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_1, asset1, approvedAssetAmount); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(issueEpochsBehind, 1); + assertEq(revokeEpochsBehind, 0); + } + + function testIssueSharesSuccess() public { + uint128 extraGasLimit = 100000; + + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_1, asset1, 500); + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.issueShares.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + expectedNavPerShare, + extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_1, asset1, extraGasLimit); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(issueEpochsBehind, 0); + assertEq(revokeEpochsBehind, 0); + } + + function testIssueSharesWithoutPendingEpochs() public { + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_1, asset1, 100000); + } + + function testInvalidShareClass() public { + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_2, asset1, 1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_2, asset1, 1, 1); + } +} diff --git a/test/managers/hub/unit/NAVManager.t.sol b/test/managers/hub/unit/NAVManager.t.sol index 2dd68d587..1d61fdf1b 100644 --- a/test/managers/hub/unit/NAVManager.t.sol +++ b/test/managers/hub/unit/NAVManager.t.sol @@ -418,12 +418,6 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { navManager.updateHoldingValue(POOL_A, SC_1, asset1); } - function testUpdateHoldingValueUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - navManager.updateHoldingValue(POOL_A, SC_1, asset1); - } - function testUpdateHoldingValuation() public { vm.expectCall( address(hub), @@ -440,7 +434,6 @@ contract NAVManagerUpdateHoldingTest is NAVManagerTest { vm.prank(unauthorized); navManager.updateHoldingValuation(POOL_A, SC_1, asset1, mockValuation); } - } contract NAVManagerCloseGainLossTest is NAVManagerTest { diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index cb90d001e..88b79bd31 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -16,9 +16,6 @@ import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; import {SimplePriceManager} from "../../../../src/managers/hub/SimplePriceManager.sol"; import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; -import {ISimplePriceManagerBase} from "../../../../src/managers/hub/interfaces/ISimplePriceManagerBase.sol"; - -import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; import "forge-std/Test.sol"; @@ -59,7 +56,6 @@ contract SimplePriceManagerTest is Test { address gateway = address(new IsContract()); address hubRegistry = address(new IsContract()); address shareClassManager = address(new IsContract()); - address batchRequestManager = address(new IsContract()); address hubHelpers = address(new IsContract()); address crosschainBatcher = address(new MockCrosschainBatcher()); @@ -82,18 +78,10 @@ contract SimplePriceManagerTest is Test { vm.mockCall(hub, abi.encodeWithSelector(IHub.gateway.selector), abi.encode(gateway)); vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); - vm.mockCall( - hub, abi.encodeWithSelector(IHub.pricePoolPerAsset.selector, POOL_A, SC_1, asset1), abi.encode(d18(1, 1)) - ); vm.mockCall(gateway, abi.encodeWithSelector(IGateway.startBatching.selector), abi.encode()); vm.mockCall(gateway, abi.encodeWithSelector(IGateway.endBatching.selector), abi.encode()); - vm.mockCall( - hubRegistry, - abi.encodeWithSelector(IHubRegistry.hubRequestManager.selector), - abi.encode(batchRequestManager) - ); vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); vm.mockCall( hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_A, hubManager), abi.encode(true) @@ -119,45 +107,6 @@ contract SimplePriceManagerTest is Test { abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_2), abi.encode(200) ); - - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), - abi.encode(1) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), - abi.encode(1) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.approveDeposits.selector), - abi.encode(uint256(0)) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.issueShares.selector), - abi.encode(uint256(0)) - ); - vm.mockCall( - batchRequestManager, abi.encodeWithSelector(IBatchRequestManager.approveRedeems.selector), abi.encode() - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.revokeShares.selector), - abi.encode(uint256(0)) - ); } function _deployManager() internal { @@ -191,7 +140,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { networks[0] = CENTRIFUGE_ID_1; vm.expectEmit(true, true, true, true); - emit ISimplePriceManagerBase.UpdateNetworks(POOL_A, networks); + emit ISimplePriceManager.UpdateNetworks(POOL_A, networks); vm.prank(hubManager); priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); @@ -205,7 +154,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { networks2[1] = CENTRIFUGE_ID_2; vm.expectEmit(true, true, true, true); - emit ISimplePriceManagerBase.UpdateNetworks(POOL_A, networks2); + emit ISimplePriceManager.UpdateNetworks(POOL_A, networks2); vm.prank(hubManager); priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_2); @@ -232,7 +181,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_B, hubManager), abi.encode(true) ); - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClassCount.selector); + vm.expectRevert(ISimplePriceManager.InvalidShareClassCount.selector); vm.prank(hubManager); priceManager.addNetwork(POOL_B, CENTRIFUGE_ID_1); } @@ -291,7 +240,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { vm.prank(hubManager); priceManager.addNetwork(POOL_A, CENTRIFUGE_ID_1); - vm.expectRevert(ISimplePriceManagerBase.NetworkNotFound.selector); + vm.expectRevert(ISimplePriceManager.NetworkNotFound.selector); vm.prank(hubManager); priceManager.removeNetwork(POOL_A, CENTRIFUGE_ID_2); } @@ -300,7 +249,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { address newManager = makeAddr("newManager"); vm.expectEmit(true, true, false, false); - emit ISimplePriceManagerBase.UpdateManager(POOL_A, newManager, true); + emit ISimplePriceManager.UpdateManager(POOL_A, newManager, true); vm.prank(hubManager); priceManager.updateManager(POOL_A, newManager, true); @@ -316,7 +265,7 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { assertTrue(priceManager.manager(POOL_A, managerAddr)); vm.expectEmit(true, true, false, false); - emit ISimplePriceManagerBase.UpdateManager(POOL_A, managerAddr, false); + emit ISimplePriceManager.UpdateManager(POOL_A, managerAddr, false); vm.prank(hubManager); priceManager.updateManager(POOL_A, managerAddr, false); @@ -338,7 +287,7 @@ contract SimplePriceManagerFileTests is SimplePriceManagerTest { address newCrosschainBatcher = makeAddr("newCrosschainBatcher"); vm.expectEmit(true, false, true, true); - emit ISimplePriceManagerBase.File("crosschainBatcher", newCrosschainBatcher); + emit ISimplePriceManager.File("crosschainBatcher", newCrosschainBatcher); vm.prank(auth); priceManager.file("crosschainBatcher", newCrosschainBatcher); @@ -349,7 +298,7 @@ contract SimplePriceManagerFileTests is SimplePriceManagerTest { function testFileUnrecognizedParam() public { address someAddress = makeAddr("someAddress"); - vm.expectRevert(ISimplePriceManagerBase.FileUnrecognizedParam.selector); + vm.expectRevert(ISimplePriceManager.FileUnrecognizedParam.selector); vm.prank(auth); priceManager.file("invalid", someAddress); } @@ -388,7 +337,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { ); vm.expectEmit(true, true, true, true); - emit ISimplePriceManagerBase.Update(POOL_A, SC_1, netAssetValue, 100, d18(10, 1)); + emit ISimplePriceManager.Update(POOL_A, SC_1, netAssetValue, 100, d18(10, 1)); vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, netAssetValue); @@ -412,7 +361,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.expectCall(address(hub), abi.encodeWithSelector(IHub.updateSharePrice.selector, POOL_A, SC_1, d18(9, 1))); vm.expectEmit(true, true, true, true); - emit ISimplePriceManagerBase.Update(POOL_A, SC_1, 2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 + emit ISimplePriceManager.Update(POOL_A, SC_1, 2700, 300, d18(9, 1)); // total NAV=2700, total issuance=300 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_2, netAssetValue2); @@ -435,7 +384,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { uint128 newNetAssetValue = 1200; vm.expectEmit(true, true, true, true); - emit ISimplePriceManagerBase.Update(POOL_A, SC_1, 1200, 150, d18(8, 1)); // 1200/150 = 8 + emit ISimplePriceManager.Update(POOL_A, SC_1, 1200, 150, d18(8, 1)); // 1200/150 = 8 vm.prank(caller); priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, newNetAssetValue); @@ -469,7 +418,7 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { } function testInvalidShareClass() public { - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); vm.prank(caller); priceManager.onUpdate(POOL_A, SC_2, CENTRIFUGE_ID_1, 1000); } @@ -490,7 +439,7 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { uint128 sharesTransferred = 50; vm.expectEmit(true, true, false, true); - emit ISimplePriceManagerBase.Transfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); + emit ISimplePriceManager.Transfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); vm.prank(caller); priceManager.onTransfer(POOL_A, SC_1, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, sharesTransferred); @@ -524,271 +473,8 @@ contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { } function testInvalidShareClass() public { - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); vm.prank(caller); priceManager.onTransfer(POOL_A, SC_2, CENTRIFUGE_ID_1, CENTRIFUGE_ID_2, 50); } } - -contract SimplePriceManagerInvestorActionsTest is SimplePriceManagerTest { - D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 - - function setUp() public override { - super.setUp(); - - vm.prank(caller); - priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); - } - - function testApproveDepositsAndIssueSharesSuccess() public { - uint128 approvedAssetAmount = 500; - uint128 extraGasLimit = 100000; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveDeposits.selector, POOL_A, SC_1, asset1, 1, approvedAssetAmount, d18(1, 1) - ) - ); - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.issueShares.selector, - POOL_A, - SC_1, - asset1, - uint32(1), - expectedNavPerShare, - extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, approvedAssetAmount, extraGasLimit); - } - - function testApproveDepositsAndIssueSharesUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); - } - - function testApproveDepositsAndIssueSharesMismatchedEpochs() public { - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), - abi.encode(1) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - - vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); - } - - function testApproveRedeemsAndRevokeSharesSuccess() public { - uint128 approvedShareAmount = 50; - uint128 extraGasLimit = 100000; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveRedeems.selector, - POOL_A, - SC_1, - asset1, - uint32(2), - approvedShareAmount, - d18(1, 1) - ) - ); - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.revokeShares.selector, - POOL_A, - SC_1, - asset1, - uint32(2), - expectedNavPerShare, - extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, approvedShareAmount, extraGasLimit); - } - - function testApproveRedeemsAndRevokeSharesUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); - } - - function testApproveRedeemsAndRevokeSharesMismatchedEpochs() public { - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), - abi.encode(3) - ); - - vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); - } - - function testApproveRedeemsSuccess() public { - uint128 approvedShareAmount = 50; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveRedeems.selector, - POOL_A, - SC_1, - asset1, - uint32(2), - approvedShareAmount, - d18(1, 1) - ) - ); - - vm.prank(manager); - priceManager.approveRedeems(POOL_A, SC_1, asset1, approvedShareAmount); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(revokeEpochsBehind, 1); - assertEq(issueEpochsBehind, 0); - } - - function testApproveRedeemsUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); - } - - function testRevokeSharesSuccess() public { - uint128 extraGasLimit = 100000; - - vm.prank(manager); - priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.revokeShares(POOL_A, SC_1, asset1, extraGasLimit); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(revokeEpochsBehind, 0); - assertEq(issueEpochsBehind, 0); - } - - function testRevokeSharesUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); - } - - function testRevokeSharesWithoutPendingEpochs() public { - vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); - } - - function testApproveDepositsSuccess() public { - uint128 approvedAssetAmount = 500; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveDeposits.selector, - POOL_A, - SC_1, - asset1, - uint32(1), - approvedAssetAmount, - d18(1, 1) - ) - ); - - vm.prank(manager); - priceManager.approveDeposits(POOL_A, SC_1, asset1, approvedAssetAmount); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(issueEpochsBehind, 1); - assertEq(revokeEpochsBehind, 0); - } - - function testIssueSharesSuccess() public { - uint128 extraGasLimit = 100000; - - vm.prank(manager); - priceManager.approveDeposits(POOL_A, SC_1, asset1, 500); - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.issueShares.selector, - POOL_A, - SC_1, - asset1, - uint32(1), - expectedNavPerShare, - extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.issueShares(POOL_A, SC_1, asset1, extraGasLimit); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(issueEpochsBehind, 0); - assertEq(revokeEpochsBehind, 0); - } - - function testIssueSharesWithoutPendingEpochs() public { - vm.expectRevert(ISimplePriceManagerBase.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.issueShares(POOL_A, SC_1, asset1, 100000); - } - - function testInvalidShareClass() public { - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveDeposits(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); - vm.prank(manager); - priceManager.issueShares(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveRedeems(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); - vm.prank(manager); - priceManager.revokeShares(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_2, asset1, 1, 1); - - vm.expectRevert(ISimplePriceManagerBase.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_2, asset1, 1, 1); - } -} From c7c03afec596f5f4781680dc9793b9c88aa70636 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:40:23 +0200 Subject: [PATCH 74/83] withBatch --- script/HubManagersDeployer.s.sol | 5 +- src/managers/hub/BatchSimplePriceManager.sol | 56 +++++++++++++------ src/managers/hub/SimplePriceManager.sol | 23 ++++---- .../interfaces/IBatchSimplePriceManager.sol | 10 ++-- .../managers/hub/integration/NAVManager.t.sol | 2 +- .../hub/unit/BatchSimplePriceManager.t.sol | 15 +++-- .../hub/unit/SimplePriceManager.t.sol | 38 +++++++------ 7 files changed, 84 insertions(+), 65 deletions(-) diff --git a/script/HubManagersDeployer.s.sol b/script/HubManagersDeployer.s.sol index a737fe002..3922bcfc8 100644 --- a/script/HubManagersDeployer.s.sol +++ b/script/HubManagersDeployer.s.sol @@ -26,7 +26,6 @@ contract HubManagersActionBatcher is HubActionBatcher { report.navManager.rely(address(report.hub.hubHandler)); report.navManager.rely(address(report.hub.holdings)); report.simplePriceManager.rely(address(report.navManager)); - report.simplePriceManager.rely(address(report.hub.common.crosschainBatcher)); } function revokeManagers(HubManagersReport memory report) public onlyDeployer { @@ -57,9 +56,7 @@ contract HubManagersDeployer is HubDeployer { simplePriceManager = SimplePriceManager( create3( generateSalt("simplePriceManager"), - abi.encodePacked( - type(SimplePriceManager).creationCode, abi.encode(hub, crosschainBatcher, address(batcher)) - ) + abi.encodePacked(type(SimplePriceManager).creationCode, abi.encode(hub, address(batcher))) ) ); diff --git a/src/managers/hub/BatchSimplePriceManager.sol b/src/managers/hub/BatchSimplePriceManager.sol index a482132ee..154dc43c0 100644 --- a/src/managers/hub/BatchSimplePriceManager.sol +++ b/src/managers/hub/BatchSimplePriceManager.sol @@ -10,17 +10,18 @@ import {D18, d18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; - import {IHub} from "../../hub/interfaces/IHub.sol"; import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; /// @notice Simple price manager for single share class pools with async request management. contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager { - constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) - SimplePriceManager(hub_, crosschainBatcher_, deployer) - {} + constructor(IHub hub_, address deployer) SimplePriceManager(hub_, deployer) {} + + modifier onlyGateway() { + require(msg.sender == address(gateway), NotAuthorized()); + _; + } //---------------------------------------------------------------------------------------------- // Updates @@ -46,7 +47,7 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager /// @inheritdoc IBatchSimplePriceManager function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) - external + external payable onlyManager(poolId) { require(scId.index() == 1, InvalidShareClass()); @@ -59,14 +60,14 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager networkMetrics_.issueEpochsBehind++; D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); - requestManager.approveDeposits( - poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset + requestManager.approveDeposits{value: msg.value}( + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset, msg.sender ); } /// @inheritdoc IBatchSimplePriceManager function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) - external + external payable onlyManager(poolId) { require(scId.index() == 1, InvalidShareClass()); @@ -80,7 +81,7 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager networkMetrics_.issueEpochsBehind--; D18 navPoolPerShare = _navPerShare(poolId); - requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + requestManager.issueShares{value: msg.value}(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit, msg.sender); } /// @inheritdoc IBatchSimplePriceManager @@ -105,7 +106,7 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager /// @inheritdoc IBatchSimplePriceManager function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) - external + external payable onlyManager(poolId) { require(scId.index() == 1, InvalidShareClass()); @@ -119,7 +120,7 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager networkMetrics_.revokeEpochsBehind--; D18 navPoolPerShare = _navPerShare(poolId); - requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + requestManager.revokeShares{value: msg.value}(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit, msg.sender); } /// @inheritdoc IBatchSimplePriceManager @@ -129,7 +130,28 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager AssetId depositAssetId, uint128 approvedAssetAmount, uint128 extraGasLimit - ) external onlyManager(poolId) { + ) external payable onlyManager(poolId) { + gateway.withBatch{value: msg.value}( + abi.encodeWithSelector( + BatchSimplePriceManager.approveDepositsAndIssueSharesCallback.selector, + poolId, + scId, + depositAssetId, + approvedAssetAmount, + extraGasLimit + ), + msg.sender + ); + } + + + function approveDepositsAndIssueSharesCallback( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external onlyGateway { require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); @@ -141,9 +163,9 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); D18 navPoolPerShare = _navPerShare(poolId); requestManager.approveDeposits( - poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset, address(0) ); - requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit); + requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit, address(0)); } /// @inheritdoc IBatchSimplePriceManager @@ -153,7 +175,7 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager AssetId payoutAssetId, uint128 approvedShareAmount, uint128 extraGasLimit - ) external onlyManager(poolId) { + ) external payable onlyManager(poolId) { require(scId.index() == 1, InvalidShareClass()); IBatchRequestManager requestManager = IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); @@ -167,6 +189,6 @@ contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager requestManager.approveRedeems( poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset ); - requestManager.revokeShares(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit); + requestManager.revokeShares{value: msg.value}(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit, msg.sender); } } diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 9009d10e0..fa6f1097a 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -10,7 +10,7 @@ import {D18, d18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; import {AssetId} from "../../common/types/AssetId.sol"; import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {ICrosschainBatcher} from "../../common/interfaces/ICrosschainBatcher.sol"; +import {IGateway} from "../../common/interfaces/IGateway.sol"; import {IHub} from "../../hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; @@ -18,7 +18,7 @@ import {IShareClassManager} from "../../hub/interfaces/IShareClassManager.sol"; /// @notice Base share price calculation manager for single share class pools. contract SimplePriceManager is ISimplePriceManager, Auth { - ICrosschainBatcher public crosschainBatcher; + IGateway public gateway; IHub public immutable hub; IHubRegistry public immutable hubRegistry; IShareClassManager public immutable shareClassManager; @@ -27,9 +27,9 @@ contract SimplePriceManager is ISimplePriceManager, Auth { mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; mapping(PoolId poolId => mapping(address => bool)) public manager; - constructor(IHub hub_, ICrosschainBatcher crosschainBatcher_, address deployer) Auth(deployer) { + constructor(IHub hub_, address deployer) Auth(deployer) { hub = hub_; - crosschainBatcher = crosschainBatcher_; + gateway = hub_.gateway(); hubRegistry = hub_.hubRegistry(); shareClassManager = hub_.shareClassManager(); } @@ -50,7 +50,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc ISimplePriceManager function file(bytes32 what, address data) external auth { - if (what == "crosschainBatcher") crosschainBatcher = ICrosschainBatcher(data); + if (what == "gateway") gateway = IGateway(data); else revert ISimplePriceManager.FileUnrecognizedParam(); emit File(what, data); } @@ -111,17 +111,16 @@ contract SimplePriceManager is ISimplePriceManager, Auth { { require(scId.index() == 1, InvalidShareClass()); - crosschainBatcher.execute( + gateway.withBatch( abi.encodeWithSelector( SimplePriceManager.onUpdateCallback.selector, poolId, scId, centrifugeId, netAssetValue - ) + ), + address(0) ); } - function onUpdateCallback(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) - external - auth - { + function onUpdateCallback(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) external { + require(msg.sender == address(gateway), NotAuthorized()); NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][centrifugeId]; Metrics storage metrics_ = metrics[poolId]; uint128 issuance = shareClassManager.issuance(scId, centrifugeId); @@ -138,7 +137,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { hub.updateSharePrice(poolId, scId, price); for (uint256 i; i < networkCount; i++) { - hub.notifySharePrice(poolId, scId, metrics_.networks[i]); + hub.notifySharePrice(poolId, scId, metrics_.networks[i], address(0)); } emit Update(poolId, scId, metrics_.netAssetValue, metrics_.issuance, price); diff --git a/src/managers/hub/interfaces/IBatchSimplePriceManager.sol b/src/managers/hub/interfaces/IBatchSimplePriceManager.sol index 93a285711..1497575be 100644 --- a/src/managers/hub/interfaces/IBatchSimplePriceManager.sol +++ b/src/managers/hub/interfaces/IBatchSimplePriceManager.sol @@ -18,14 +18,14 @@ interface IBatchSimplePriceManager is ISimplePriceManager { /// @param depositAssetId The asset ID for deposits /// @param approvedAssetAmount Amount of assets to approve for deposit function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) - external; + external payable; /// @notice Issue shares for approved deposit epochs /// @param poolId The pool ID /// @param scId The share class ID /// @param depositAssetId The asset ID for deposits /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external; + function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external payable; /// @notice Approve redemption requests for a given share amount /// @param poolId The pool ID @@ -40,7 +40,7 @@ interface IBatchSimplePriceManager is ISimplePriceManager { /// @param scId The share class ID /// @param payoutAssetId The asset ID for payouts /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external; + function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external payable; /// @notice Approve deposits and issue shares in sequence using current NAV per share /// @param poolId The pool ID @@ -54,7 +54,7 @@ interface IBatchSimplePriceManager is ISimplePriceManager { AssetId depositAssetId, uint128 approvedAssetAmount, uint128 extraGasLimit - ) external; + ) external payable; /// @notice Approve redeems and revoke shares in sequence using current NAV per share /// @param poolId The pool ID @@ -68,5 +68,5 @@ interface IBatchSimplePriceManager is ISimplePriceManager { AssetId payoutAssetId, uint128 approvedShareAmount, uint128 extraGasLimit - ) external; + ) external payable; } diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index edc73a670..5fdd0c92d 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -160,7 +160,7 @@ contract NAVManagerIntegrationTest is BaseTest { assertEq(globalIssuance, 3800e18); vm.prank(address(root)); - hubHandler.initiateTransferShares(CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0); + hubHandler.initiateTransferShares(CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0, address(0)); navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); diff --git a/test/managers/hub/unit/BatchSimplePriceManager.t.sol b/test/managers/hub/unit/BatchSimplePriceManager.t.sol index 3b5dc200a..635fec74d 100644 --- a/test/managers/hub/unit/BatchSimplePriceManager.t.sol +++ b/test/managers/hub/unit/BatchSimplePriceManager.t.sol @@ -6,9 +6,9 @@ import {Multicall} from "../../../../src/misc/Multicall.sol"; import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; -import {ICrosschainBatcher} from "../../../../src/common/interfaces/ICrosschainBatcher.sol"; import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; @@ -24,9 +24,9 @@ import "forge-std/Test.sol"; contract IsContract {} -contract MockCrosschainBatcher { - function execute(bytes memory data) external payable returns (uint256 cost) { - (bool success, bytes memory returnData) = msg.sender.call{value: msg.value}(data); +contract MockGateway { + function withBatch(bytes memory data, address) external payable returns (uint256 cost) { + (bool success, bytes memory returnData) = msg.sender.call(data); if (!success) { uint256 length = returnData.length; require(length != 0, "Empty revert"); @@ -52,10 +52,10 @@ contract BatchSimplePriceManagerTest is Test { AssetId asset1 = newAssetId(1, 1); address hub = address(new MockHub()); + address gateway = address(new MockGateway()); address hubRegistry = address(new IsContract()); address shareClassManager = address(new IsContract()); address batchRequestManager = address(new IsContract()); - address crosschainBatcher = address(new MockCrosschainBatcher()); address unauthorized = makeAddr("unauthorized"); address hubManager = makeAddr("hubManager"); @@ -73,6 +73,7 @@ contract BatchSimplePriceManagerTest is Test { function _setupMocks() internal { vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + vm.mockCall(hub, abi.encodeWithSelector(IBatchedMulticall.gateway.selector), abi.encode(gateway)); vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); vm.mockCall( @@ -136,11 +137,9 @@ contract BatchSimplePriceManagerTest is Test { } function _deployManager() internal { - priceManager = new BatchSimplePriceManager(IHub(hub), ICrosschainBatcher(crosschainBatcher), auth); + priceManager = new BatchSimplePriceManager(IHub(hub), auth); vm.prank(auth); priceManager.rely(caller); - vm.prank(auth); - priceManager.rely(crosschainBatcher); vm.prank(hubManager); priceManager.updateManager(POOL_A, manager, true); diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index 88b79bd31..f985f1d6c 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -7,8 +7,8 @@ import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; import {PoolId} from "../../../../src/common/types/PoolId.sol"; import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; +import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; -import {ICrosschainBatcher} from "../../../../src/common/interfaces/ICrosschainBatcher.sol"; import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; @@ -21,8 +21,8 @@ import "forge-std/Test.sol"; contract IsContract {} -contract MockCrosschainBatcher { - function execute(bytes memory data) external payable returns (uint256 cost) { +contract MockGateway { + function withBatch(bytes memory data, address) external payable returns (uint256 cost) { (bool success, bytes memory returnData) = msg.sender.call{value: msg.value}(data); if (!success) { uint256 length = returnData.length; @@ -53,11 +53,10 @@ contract SimplePriceManagerTest is Test { AssetId asset2 = newAssetId(2, 1); address hub = address(new MockHub()); - address gateway = address(new IsContract()); + address gateway = address(new MockGateway()); address hubRegistry = address(new IsContract()); address shareClassManager = address(new IsContract()); address hubHelpers = address(new IsContract()); - address crosschainBatcher = address(new MockCrosschainBatcher()); address unauthorized = makeAddr("unauthorized"); address hubManager = makeAddr("hubManager"); @@ -75,13 +74,10 @@ contract SimplePriceManagerTest is Test { function _setupMocks() internal { vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); - vm.mockCall(hub, abi.encodeWithSelector(IHub.gateway.selector), abi.encode(gateway)); + vm.mockCall(hub, abi.encodeWithSelector(IBatchedMulticall.gateway.selector), abi.encode(gateway)); vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); - vm.mockCall(gateway, abi.encodeWithSelector(IGateway.startBatching.selector), abi.encode()); - vm.mockCall(gateway, abi.encodeWithSelector(IGateway.endBatching.selector), abi.encode()); - vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); vm.mockCall( hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_A, hubManager), abi.encode(true) @@ -110,11 +106,11 @@ contract SimplePriceManagerTest is Test { } function _deployManager() internal { - priceManager = new SimplePriceManager(IHub(hub), ICrosschainBatcher(crosschainBatcher), auth); + priceManager = new SimplePriceManager(IHub(hub), auth); vm.prank(auth); priceManager.rely(caller); vm.prank(auth); - priceManager.rely(crosschainBatcher); + priceManager.rely(gateway); vm.prank(hubManager); priceManager.updateManager(POOL_A, manager, true); @@ -283,16 +279,16 @@ contract SimplePriceManagerConfigureTest is SimplePriceManagerTest { } contract SimplePriceManagerFileTests is SimplePriceManagerTest { - function testFileCrosschainBatcher() public { - address newCrosschainBatcher = makeAddr("newCrosschainBatcher"); + function testFileGateway() public { + address newGateway = makeAddr("newGateway"); vm.expectEmit(true, false, true, true); - emit ISimplePriceManager.File("crosschainBatcher", newCrosschainBatcher); + emit ISimplePriceManager.File("gateway", newGateway); vm.prank(auth); - priceManager.file("crosschainBatcher", newCrosschainBatcher); + priceManager.file("gateway", newGateway); - assertEq(address(priceManager.crosschainBatcher()), newCrosschainBatcher); + assertEq(address(priceManager.gateway()), newGateway); } function testFileUnrecognizedParam() public { @@ -304,11 +300,11 @@ contract SimplePriceManagerFileTests is SimplePriceManagerTest { } function testFileUnauthorized() public { - address newCrosschainBatcher = makeAddr("newCrosschainBatcher"); + address newGateway = makeAddr("newGateway"); vm.expectRevert(IAuth.NotAuthorized.selector); vm.prank(unauthorized); - priceManager.file("gateway", newCrosschainBatcher); + priceManager.file("gateway", newGateway); } } @@ -422,6 +418,12 @@ contract SimplePriceManagerOnUpdateTest is SimplePriceManagerTest { vm.prank(caller); priceManager.onUpdate(POOL_A, SC_2, CENTRIFUGE_ID_1, 1000); } + + function testCallbackNotAuthorised() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(auth); + priceManager.onUpdateCallback(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + } } contract SimplePriceManagerOnTransferTest is SimplePriceManagerTest { From f7060ef99ca976beac55907a5b843baa0c68f585 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:23:08 +0200 Subject: [PATCH 75/83] move QueueManager --- script/SpokeManagersDeployer.s.sol | 16 +++++++----- src/managers/{ => spoke}/QueueManager.sol | 26 +++++++++---------- .../spoke/interfaces/IQueueManager.sol | 6 ++--- .../spoke/integration/QueueManager.t.sol | 10 +++---- test/managers/spoke/unit/QueueManager.t.sol | 24 ++++++++--------- 5 files changed, 43 insertions(+), 39 deletions(-) rename src/managers/{ => spoke}/QueueManager.sol (84%) diff --git a/script/SpokeManagersDeployer.s.sol b/script/SpokeManagersDeployer.s.sol index 376fc733f..984ac32f7 100644 --- a/script/SpokeManagersDeployer.s.sol +++ b/script/SpokeManagersDeployer.s.sol @@ -20,7 +20,7 @@ struct SpokeManagersReport { } contract SpokeManagersActionBatcher is SpokeActionBatcher { - function engageManagers(ManagersReport memory report) public onlyDeployer { + function engageManagers(SpokeManagersReport memory report) public onlyDeployer { // rely QueueManager on Gateway report.spoke.common.gateway.rely(address(report.queueManager)); @@ -28,7 +28,7 @@ contract SpokeManagersActionBatcher is SpokeActionBatcher { report.queueManager.rely(address(report.spoke.common.root)); } - function revokeManagers(ManagersReport memory report) public onlyDeployer { + function revokeManagers(SpokeManagersReport memory report) public onlyDeployer { report.queueManager.deny(address(this)); } } @@ -79,7 +79,7 @@ contract SpokeManagersDeployer is SpokeDeployer { circleDecoder = CircleDecoder(create3(generateSalt("circleDecoder"), abi.encodePacked(type(CircleDecoder).creationCode))); - batcher.engageManagers(_managersReport()); + batcher.engageManagers(_spokeManagersReport()); register("queueManager", address(queueManager)); register("onOfframpManagerFactory", address(onOfframpManagerFactory)); @@ -95,13 +95,17 @@ contract SpokeManagersDeployer is SpokeDeployer { function removeSpokeManagersDeployerAccess(SpokeManagersActionBatcher batcher) public { removeSpokeDeployerAccess(batcher); - batcher.revokeManagers(_managersReport()); + batcher.revokeManagers(_spokeManagersReport()); } function _spokeManagersReport() internal view returns (SpokeManagersReport memory) { return SpokeManagersReport( - _spokeReport(), - queueManager, onOfframpManagerFactory, merkleProofManagerFactory, vaultDecoder, circleDecoder + _spokeReport(), + queueManager, + onOfframpManagerFactory, + merkleProofManagerFactory, + vaultDecoder, + circleDecoder ); } } diff --git a/src/managers/QueueManager.sol b/src/managers/spoke/QueueManager.sol similarity index 84% rename from src/managers/QueueManager.sol rename to src/managers/spoke/QueueManager.sol index 04d19e40f..dc3fa78a3 100644 --- a/src/managers/QueueManager.sol +++ b/src/managers/spoke/QueueManager.sol @@ -3,19 +3,19 @@ pragma solidity 0.8.28; import {IQueueManager} from "./interfaces/IQueueManager.sol"; -import {Auth} from "../misc/Auth.sol"; -import {CastLib} from "../misc/libraries/CastLib.sol"; -import {BitmapLib} from "../misc/libraries/BitmapLib.sol"; -import {TransientStorageLib} from "../misc/libraries/TransientStorageLib.sol"; - -import {PoolId} from "../common/types/PoolId.sol"; -import {AssetId} from "../common/types/AssetId.sol"; -import {IGateway} from "../common/interfaces/IGateway.sol"; -import {ShareClassId} from "../common/types/ShareClassId.sol"; - -import {IBalanceSheet} from "../spoke/interfaces/IBalanceSheet.sol"; -import {IUpdateContract} from "../spoke/interfaces/IUpdateContract.sol"; -import {UpdateContractMessageLib, UpdateContractType} from "../spoke/libraries/UpdateContractMessageLib.sol"; +import {Auth} from "../../misc/Auth.sol"; +import {CastLib} from "../../misc/libraries/CastLib.sol"; +import {BitmapLib} from "../../misc/libraries/BitmapLib.sol"; +import {TransientStorageLib} from "../../misc/libraries/TransientStorageLib.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {IGateway} from "../../common/interfaces/IGateway.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; + +import {IBalanceSheet} from "../../spoke/interfaces/IBalanceSheet.sol"; +import {IUpdateContract} from "../../spoke/interfaces/IUpdateContract.sol"; +import {UpdateContractMessageLib, UpdateContractType} from "../../spoke/libraries/UpdateContractMessageLib.sol"; /// @dev minDelay can be set to a non-zero value, for cases where assets or shares can be permissionlessly modified /// (e.g. if the on/off ramp manager is used, or if sync deposits are enabled). This prevents spam. diff --git a/src/managers/spoke/interfaces/IQueueManager.sol b/src/managers/spoke/interfaces/IQueueManager.sol index b4d415927..b2e542263 100644 --- a/src/managers/spoke/interfaces/IQueueManager.sol +++ b/src/managers/spoke/interfaces/IQueueManager.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.5.0; -import {PoolId} from "../../common/types/PoolId.sol"; -import {AssetId} from "../../common/types/AssetId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {PoolId} from "../../../common/types/PoolId.sol"; +import {AssetId} from "../../../common/types/AssetId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; interface IQueueManager { event UpdateQueueConfig( diff --git a/test/managers/spoke/integration/QueueManager.t.sol b/test/managers/spoke/integration/QueueManager.t.sol index 8db4f5855..f32bc3ee7 100644 --- a/test/managers/spoke/integration/QueueManager.t.sol +++ b/test/managers/spoke/integration/QueueManager.t.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {CastLib} from "../../../src/misc/libraries/CastLib.sol"; +import {CastLib} from "../../../../src/misc/libraries/CastLib.sol"; -import {AssetId} from "../../../src/common/types/AssetId.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; +import {AssetId} from "../../../../src/common/types/AssetId.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; -import "../../spoke/integration/BaseTest.sol"; +import "../../../spoke/integration/BaseTest.sol"; -import {UpdateContractMessageLib} from "../../../src/spoke/libraries/UpdateContractMessageLib.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; abstract contract QueueManagerBaseTest is BaseTest { uint128 constant DEFAULT_AMOUNT = 100_000_000; diff --git a/test/managers/spoke/unit/QueueManager.t.sol b/test/managers/spoke/unit/QueueManager.t.sol index 60351b86b..990e6accc 100644 --- a/test/managers/spoke/unit/QueueManager.t.sol +++ b/test/managers/spoke/unit/QueueManager.t.sol @@ -1,21 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.28; -import {IAuth} from "../../../src/misc/interfaces/IAuth.sol"; -import {CastLib} from "../../../src/misc/libraries/CastLib.sol"; +import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; +import {CastLib} from "../../../../src/misc/libraries/CastLib.sol"; -import {PoolId} from "../../../src/common/types/PoolId.sol"; -import {AssetId} from "../../../src/common/types/AssetId.sol"; -import {IGateway} from "../../../src/common/interfaces/IGateway.sol"; -import {ShareClassId} from "../../../src/common/types/ShareClassId.sol"; -import {IBatchedMulticall} from "../../../src/common/interfaces/IBatchedMulticall.sol"; +import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {AssetId} from "../../../../src/common/types/AssetId.sol"; +import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; +import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; +import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; -import {IBalanceSheet} from "../../../src/spoke/interfaces/IBalanceSheet.sol"; -import {IUpdateContract} from "../../../src/spoke/interfaces/IUpdateContract.sol"; -import {UpdateContractMessageLib} from "../../../src/spoke/libraries/UpdateContractMessageLib.sol"; +import {IBalanceSheet} from "../../../../src/spoke/interfaces/IBalanceSheet.sol"; +import {IUpdateContract} from "../../../../src/spoke/interfaces/IUpdateContract.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; -import {QueueManager} from "../../../src/managers/QueueManager.sol"; -import {IQueueManager} from "../../../src/managers/interfaces/IQueueManager.sol"; +import {QueueManager} from "../../../../src/managers/spoke/QueueManager.sol"; +import {IQueueManager} from "../../../../src/managers/spoke/interfaces/IQueueManager.sol"; import "forge-std/Test.sol"; From e73d9071e6ba452909cd8acdd5ffbc61a3034f56 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:43:43 +0200 Subject: [PATCH 76/83] fix test --- test/managers/hub/integration/NAVManager.t.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index 5fdd0c92d..d8c97a116 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -43,6 +43,8 @@ contract NAVManagerIntegrationTest is BaseTest { _setupMocks(); _setupPool(); + + vm.deal(address(root), 1 ether); } function _setupMocks() internal { @@ -160,7 +162,9 @@ contract NAVManagerIntegrationTest is BaseTest { assertEq(globalIssuance, 3800e18); vm.prank(address(root)); - hubHandler.initiateTransferShares(CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0, address(0)); + hubHandler.initiateTransferShares{value: 0.1 ether}( + CHAIN_CP, CHAIN_CV, POOL_A, scId, bytes32("receiver"), 130e18, 0, manager + ); navHub = navManager.netAssetValue(POOL_A, CHAIN_CP); navSpoke = navManager.netAssetValue(POOL_A, CHAIN_CV); From 3e614d91073bcc999735ea7cd61f537a722d3306 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:00:17 +0200 Subject: [PATCH 77/83] cleanup --- test/managers/hub/integration/NAVManager.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/managers/hub/integration/NAVManager.t.sol b/test/managers/hub/integration/NAVManager.t.sol index d8c97a116..91604f0f5 100644 --- a/test/managers/hub/integration/NAVManager.t.sol +++ b/test/managers/hub/integration/NAVManager.t.sol @@ -173,9 +173,9 @@ contract NAVManagerIntegrationTest is BaseTest { (globalNAV, globalIssuance) = simplePriceManager.metrics(POOL_A); // NAV and global issuance should remain unchanged, only issuance per network changes - assertEq(navHub, 250e18, "navHub3"); + assertEq(navHub, 250e18); assertEq(navSpoke, 3400e18); - assertEq(navHub2, navHub, "navHub v navHub3"); + assertEq(navHub2, navHub); assertEq(navSpoke2, navSpoke); assertEq(issuanceHub, 370e18); assertEq(issuanceSpoke, 3430e18); From b8f30c32454d87361e5c32b017a9b5efd75d83cb Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Wed, 1 Oct 2025 21:02:53 +0200 Subject: [PATCH 78/83] Apply suggestions from code review Co-authored-by: William Freudenberger --- src/managers/hub/NAVManager.sol | 4 ++-- src/managers/hub/SimplePriceManager.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index 967fe937f..9aec53c20 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -20,13 +20,13 @@ import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; /// @dev Assumes all assets in a pool are shared across all share classes, not segregated. contract NAVManager is INAVManager, Auth { IHub public immutable hub; - IHubRegistry public immutable hubRegistry; IHoldings public immutable holdings; IAccounting public immutable accounting; + IHubRegistry public immutable hubRegistry; mapping(PoolId => INAVHook) public navHook; - mapping(PoolId poolId => mapping(uint16 centrifugeId => bool)) public initialized; mapping(PoolId poolId => mapping(address => bool)) public manager; + mapping(PoolId poolId => mapping(uint16 centrifugeId => bool)) public initialized; constructor(IHub hub_, address deployer) Auth(deployer) { hub = hub_; diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index fa6f1097a..234c370df 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -24,8 +24,8 @@ contract SimplePriceManager is ISimplePriceManager, Auth { IShareClassManager public immutable shareClassManager; mapping(PoolId poolId => Metrics) public metrics; - mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; mapping(PoolId poolId => mapping(address => bool)) public manager; + mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; constructor(IHub hub_, address deployer) Auth(deployer) { hub = hub_; From 15d20accc8fed5a6b148d0a1c1a757fcb434dea6 Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:07:48 +0200 Subject: [PATCH 79/83] remove BatchSimplePriceManager --- src/managers/hub/BatchSimplePriceManager.sol | 194 --------- .../interfaces/IBatchSimplePriceManager.sol | 72 --- .../hub/unit/BatchSimplePriceManager.t.sol | 412 ------------------ 3 files changed, 678 deletions(-) delete mode 100644 src/managers/hub/BatchSimplePriceManager.sol delete mode 100644 src/managers/hub/interfaces/IBatchSimplePriceManager.sol delete mode 100644 test/managers/hub/unit/BatchSimplePriceManager.t.sol diff --git a/src/managers/hub/BatchSimplePriceManager.sol b/src/managers/hub/BatchSimplePriceManager.sol deleted file mode 100644 index 154dc43c0..000000000 --- a/src/managers/hub/BatchSimplePriceManager.sol +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {INAVHook} from "./interfaces/INAVManager.sol"; -import {SimplePriceManager} from "./SimplePriceManager.sol"; -import {IBatchSimplePriceManager} from "./interfaces/IBatchSimplePriceManager.sol"; - -import {D18, d18} from "../../misc/types/D18.sol"; - -import {PoolId} from "../../common/types/PoolId.sol"; -import {AssetId} from "../../common/types/AssetId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; -import {IHub} from "../../hub/interfaces/IHub.sol"; - -import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; - -/// @notice Simple price manager for single share class pools with async request management. -contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager { - constructor(IHub hub_, address deployer) SimplePriceManager(hub_, deployer) {} - - modifier onlyGateway() { - require(msg.sender == address(gateway), NotAuthorized()); - _; - } - - //---------------------------------------------------------------------------------------------- - // Updates - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc SimplePriceManager - function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) - public - override(SimplePriceManager, INAVHook) - auth - { - NetworkMetrics memory networkMetrics_ = networkMetrics[poolId][centrifugeId]; - - // If there are pending epochs to be issued or revoked, skip updating the share price, as it will likely be off - if (networkMetrics_.issueEpochsBehind > 0 || networkMetrics_.revokeEpochsBehind > 0) return; - - super.onUpdate(poolId, scId, centrifugeId, netAssetValue); - } - - //---------------------------------------------------------------------------------------------- - // Manager actions - //---------------------------------------------------------------------------------------------- - - /// @inheritdoc IBatchSimplePriceManager - function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) - external payable - onlyManager(poolId) - { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); - uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); - - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; - - networkMetrics_.issueEpochsBehind++; - - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); - requestManager.approveDeposits{value: msg.value}( - poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset, msg.sender - ); - } - - /// @inheritdoc IBatchSimplePriceManager - function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) - external payable - onlyManager(poolId) - { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); - uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); - - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; - - require(networkMetrics_.issueEpochsBehind > 0, MismatchedEpochs()); - networkMetrics_.issueEpochsBehind--; - - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.issueShares{value: msg.value}(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit, msg.sender); - } - - /// @inheritdoc IBatchSimplePriceManager - function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) - external - onlyManager(poolId) - { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); - uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); - - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; - - networkMetrics_.revokeEpochsBehind++; - - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); - requestManager.approveRedeems( - poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset - ); - } - - /// @inheritdoc IBatchSimplePriceManager - function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) - external payable - onlyManager(poolId) - { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); - uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); - - NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; - - require(networkMetrics_.revokeEpochsBehind > 0, MismatchedEpochs()); - networkMetrics_.revokeEpochsBehind--; - - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.revokeShares{value: msg.value}(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit, msg.sender); - } - - /// @inheritdoc IBatchSimplePriceManager - function approveDepositsAndIssueShares( - PoolId poolId, - ShareClassId scId, - AssetId depositAssetId, - uint128 approvedAssetAmount, - uint128 extraGasLimit - ) external payable onlyManager(poolId) { - gateway.withBatch{value: msg.value}( - abi.encodeWithSelector( - BatchSimplePriceManager.approveDepositsAndIssueSharesCallback.selector, - poolId, - scId, - depositAssetId, - approvedAssetAmount, - extraGasLimit - ), - msg.sender - ); - } - - - function approveDepositsAndIssueSharesCallback( - PoolId poolId, - ShareClassId scId, - AssetId depositAssetId, - uint128 approvedAssetAmount, - uint128 extraGasLimit - ) external onlyGateway { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); - uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); - uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); - - require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); - - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.approveDeposits( - poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset, address(0) - ); - requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit, address(0)); - } - - /// @inheritdoc IBatchSimplePriceManager - function approveRedeemsAndRevokeShares( - PoolId poolId, - ShareClassId scId, - AssetId payoutAssetId, - uint128 approvedShareAmount, - uint128 extraGasLimit - ) external payable onlyManager(poolId) { - require(scId.index() == 1, InvalidShareClass()); - IBatchRequestManager requestManager = - IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); - uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); - uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); - - require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); - - D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); - D18 navPoolPerShare = _navPerShare(poolId); - requestManager.approveRedeems( - poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset - ); - requestManager.revokeShares{value: msg.value}(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit, msg.sender); - } -} diff --git a/src/managers/hub/interfaces/IBatchSimplePriceManager.sol b/src/managers/hub/interfaces/IBatchSimplePriceManager.sol deleted file mode 100644 index 1497575be..000000000 --- a/src/managers/hub/interfaces/IBatchSimplePriceManager.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {ISimplePriceManager} from "./ISimplePriceManager.sol"; - -import {PoolId} from "../../../common/types/PoolId.sol"; -import {AssetId} from "../../../common/types/AssetId.sol"; -import {ShareClassId} from "../../../common/types/ShareClassId.sol"; - -interface IBatchSimplePriceManager is ISimplePriceManager { - //---------------------------------------------------------------------------------------------- - // Manager actions - //---------------------------------------------------------------------------------------------- - - /// @notice Approve deposit requests for a given asset amount - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param depositAssetId The asset ID for deposits - /// @param approvedAssetAmount Amount of assets to approve for deposit - function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) - external payable; - - /// @notice Issue shares for approved deposit epochs - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param depositAssetId The asset ID for deposits - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external payable; - - /// @notice Approve redemption requests for a given share amount - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param payoutAssetId The asset ID for payouts - /// @param approvedShareAmount Amount of shares to approve for redemption - function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) - external; - - /// @notice Revoke shares from approved redemption requests - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param payoutAssetId The asset ID for payouts - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external payable; - - /// @notice Approve deposits and issue shares in sequence using current NAV per share - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param depositAssetId The asset ID for deposits - /// @param approvedAssetAmount Amount of assets to approve - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function approveDepositsAndIssueShares( - PoolId poolId, - ShareClassId scId, - AssetId depositAssetId, - uint128 approvedAssetAmount, - uint128 extraGasLimit - ) external payable; - - /// @notice Approve redeems and revoke shares in sequence using current NAV per share - /// @param poolId The pool ID - /// @param scId The share class ID - /// @param payoutAssetId The asset ID for payouts - /// @param approvedShareAmount Amount of shares to approve for redemption - /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain - function approveRedeemsAndRevokeShares( - PoolId poolId, - ShareClassId scId, - AssetId payoutAssetId, - uint128 approvedShareAmount, - uint128 extraGasLimit - ) external payable; -} diff --git a/test/managers/hub/unit/BatchSimplePriceManager.t.sol b/test/managers/hub/unit/BatchSimplePriceManager.t.sol deleted file mode 100644 index 635fec74d..000000000 --- a/test/managers/hub/unit/BatchSimplePriceManager.t.sol +++ /dev/null @@ -1,412 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {D18, d18} from "../../../../src/misc/types/D18.sol"; -import {Multicall} from "../../../../src/misc/Multicall.sol"; -import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; - -import {PoolId} from "../../../../src/common/types/PoolId.sol"; -import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; -import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; -import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; -import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; - -import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; -import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; -import {BatchSimplePriceManager} from "../../../../src/managers/hub/BatchSimplePriceManager.sol"; -import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; -import {IBatchSimplePriceManager} from "../../../../src/managers/hub/interfaces/IBatchSimplePriceManager.sol"; -import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; - -import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; - -import "forge-std/Test.sol"; - -contract IsContract {} - -contract MockGateway { - function withBatch(bytes memory data, address) external payable returns (uint256 cost) { - (bool success, bytes memory returnData) = msg.sender.call(data); - if (!success) { - uint256 length = returnData.length; - require(length != 0, "Empty revert"); - - assembly ("memory-safe") { - revert(add(32, returnData), length) - } - } - return 0; - } -} - -contract MockHub is Multicall { - function notifySharePrice(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external payable {} -} - -contract BatchSimplePriceManagerTest is Test { - PoolId constant POOL_A = PoolId.wrap(1); - ShareClassId immutable SC_1 = newShareClassId(POOL_A, 1); - ShareClassId immutable SC_2 = newShareClassId(POOL_A, 2); - uint16 constant CENTRIFUGE_ID_1 = 1; - - AssetId asset1 = newAssetId(1, 1); - - address hub = address(new MockHub()); - address gateway = address(new MockGateway()); - address hubRegistry = address(new IsContract()); - address shareClassManager = address(new IsContract()); - address batchRequestManager = address(new IsContract()); - - address unauthorized = makeAddr("unauthorized"); - address hubManager = makeAddr("hubManager"); - address manager = makeAddr("manager"); - address caller = makeAddr("caller"); - address auth = makeAddr("auth"); - - BatchSimplePriceManager priceManager; - - function setUp() public virtual { - _setupMocks(); - _deployManager(); - } - - function _setupMocks() internal { - vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); - vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); - vm.mockCall(hub, abi.encodeWithSelector(IBatchedMulticall.gateway.selector), abi.encode(gateway)); - vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); - vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); - vm.mockCall( - hub, abi.encodeWithSelector(IHub.pricePoolPerAsset.selector, POOL_A, SC_1, asset1), abi.encode(d18(1, 1)) - ); - - vm.mockCall( - hubRegistry, - abi.encodeWithSelector(IHubRegistry.hubRequestManager.selector), - abi.encode(batchRequestManager) - ); - vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); - vm.mockCall( - hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_A, hubManager), abi.encode(true) - ); - - vm.mockCall( - shareClassManager, - abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_1), - abi.encode(100) - ); - - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), - abi.encode(1) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), - abi.encode(1) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.approveDeposits.selector), - abi.encode(uint256(0)) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.issueShares.selector), - abi.encode(uint256(0)) - ); - vm.mockCall( - batchRequestManager, abi.encodeWithSelector(IBatchRequestManager.approveRedeems.selector), abi.encode() - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.revokeShares.selector), - abi.encode(uint256(0)) - ); - } - - function _deployManager() internal { - priceManager = new BatchSimplePriceManager(IHub(hub), auth); - vm.prank(auth); - priceManager.rely(caller); - - vm.prank(hubManager); - priceManager.updateManager(POOL_A, manager, true); - - vm.deal(address(priceManager), 1 ether); - } -} - -contract BatchSimplePriceManagerInvestorActionsTest is BatchSimplePriceManagerTest { - D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 - - function setUp() public override { - super.setUp(); - - vm.prank(caller); - priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); - } - - function testApproveDepositsAndIssueSharesSuccess() public { - uint128 approvedAssetAmount = 500; - uint128 extraGasLimit = 100000; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveDeposits.selector, POOL_A, SC_1, asset1, 1, approvedAssetAmount, d18(1, 1) - ) - ); - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.issueShares.selector, - POOL_A, - SC_1, - asset1, - uint32(1), - expectedNavPerShare, - extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, approvedAssetAmount, extraGasLimit); - } - - function testApproveDepositsAndIssueSharesUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); - } - - function testApproveDepositsAndIssueSharesMismatchedEpochs() public { - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), - abi.encode(1) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); - } - - function testApproveRedeemsAndRevokeSharesSuccess() public { - uint128 approvedShareAmount = 50; - uint128 extraGasLimit = 100000; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveRedeems.selector, - POOL_A, - SC_1, - asset1, - uint32(2), - approvedShareAmount, - d18(1, 1) - ) - ); - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.revokeShares.selector, - POOL_A, - SC_1, - asset1, - uint32(2), - expectedNavPerShare, - extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, approvedShareAmount, extraGasLimit); - } - - function testApproveRedeemsAndRevokeSharesUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); - } - - function testApproveRedeemsAndRevokeSharesMismatchedEpochs() public { - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), - abi.encode(2) - ); - vm.mockCall( - batchRequestManager, - abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), - abi.encode(3) - ); - - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); - } - - function testApproveRedeemsSuccess() public { - uint128 approvedShareAmount = 50; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveRedeems.selector, - POOL_A, - SC_1, - asset1, - uint32(2), - approvedShareAmount, - d18(1, 1) - ) - ); - - vm.prank(manager); - priceManager.approveRedeems(POOL_A, SC_1, asset1, approvedShareAmount); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(revokeEpochsBehind, 1); - assertEq(issueEpochsBehind, 0); - } - - function testApproveRedeemsUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); - } - - function testRevokeSharesSuccess() public { - uint128 extraGasLimit = 100000; - - vm.prank(manager); - priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.revokeShares(POOL_A, SC_1, asset1, extraGasLimit); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(revokeEpochsBehind, 0); - assertEq(issueEpochsBehind, 0); - } - - function testRevokeSharesUnauthorized() public { - vm.expectRevert(IAuth.NotAuthorized.selector); - vm.prank(unauthorized); - priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); - } - - function testRevokeSharesWithoutPendingEpochs() public { - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); - } - - function testApproveDepositsSuccess() public { - uint128 approvedAssetAmount = 500; - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.approveDeposits.selector, - POOL_A, - SC_1, - asset1, - uint32(1), - approvedAssetAmount, - d18(1, 1) - ) - ); - - vm.prank(manager); - priceManager.approveDeposits(POOL_A, SC_1, asset1, approvedAssetAmount); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(issueEpochsBehind, 1); - assertEq(revokeEpochsBehind, 0); - } - - function testIssueSharesSuccess() public { - uint128 extraGasLimit = 100000; - - vm.prank(manager); - priceManager.approveDeposits(POOL_A, SC_1, asset1, 500); - - vm.expectCall( - address(batchRequestManager), - abi.encodeWithSelector( - IBatchRequestManager.issueShares.selector, - POOL_A, - SC_1, - asset1, - uint32(1), - expectedNavPerShare, - extraGasLimit - ) - ); - - vm.prank(manager); - priceManager.issueShares(POOL_A, SC_1, asset1, extraGasLimit); - - (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); - assertEq(issueEpochsBehind, 0); - assertEq(revokeEpochsBehind, 0); - } - - function testIssueSharesWithoutPendingEpochs() public { - vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); - vm.prank(manager); - priceManager.issueShares(POOL_A, SC_1, asset1, 100000); - } - - function testInvalidShareClass() public { - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveDeposits(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); - vm.prank(manager); - priceManager.issueShares(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveRedeems(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); - vm.prank(manager); - priceManager.revokeShares(POOL_A, SC_2, asset1, 1); - - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveDepositsAndIssueShares(POOL_A, SC_2, asset1, 1, 1); - - vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); - vm.prank(manager); - priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_2, asset1, 1, 1); - } -} From e1a2d0620ca2d4d146cbab426c174423c8a7608a Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:43:32 +0200 Subject: [PATCH 80/83] remove _onSync --- src/managers/hub/NAVManager.sol | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index 9aec53c20..d27720b1f 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -127,7 +127,12 @@ contract NAVManager is INAVManager, Auth { /// @inheritdoc ISnapshotHook function onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external auth { - _onSync(poolId, scId, centrifugeId); + require(address(navHook[poolId]) != address(0), InvalidNAVHook()); + + uint128 netAssetValue_ = netAssetValue(poolId, centrifugeId); + navHook[poolId].onUpdate(poolId, scId, centrifugeId, netAssetValue_); + + emit Sync(poolId, scId, centrifugeId, netAssetValue_); } /// @inheritdoc ISnapshotHook @@ -243,17 +248,4 @@ contract NAVManager is INAVManager, Auth { function lossAccount(uint16 centrifugeId) public pure returns (AccountId) { return withCentrifugeId(centrifugeId, uint16(AccountType.Loss)); } - - //---------------------------------------------------------------------------------------------- - // Internal methods - //---------------------------------------------------------------------------------------------- - - function _onSync(PoolId poolId, ShareClassId scId, uint16 centrifugeId) internal { - require(address(navHook[poolId]) != address(0), InvalidNAVHook()); - - uint128 netAssetValue_ = netAssetValue(poolId, centrifugeId); - navHook[poolId].onUpdate(poolId, scId, centrifugeId, netAssetValue_); - - emit Sync(poolId, scId, centrifugeId, netAssetValue_); - } } From fdaed4cbeb6cd59ea038d0e2d57b6515588991df Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:45:52 +0200 Subject: [PATCH 81/83] organize --- src/managers/hub/SimplePriceManager.sol | 3 +-- src/managers/hub/interfaces/ISimplePriceManager.sol | 1 - test/managers/hub/unit/SimplePriceManager.t.sol | 5 ++--- test/managers/spoke/unit/QueueManager.t.sol | 5 ++--- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 234c370df..045981964 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -8,9 +8,8 @@ import {Auth} from "../../misc/Auth.sol"; import {D18, d18} from "../../misc/types/D18.sol"; import {PoolId} from "../../common/types/PoolId.sol"; -import {AssetId} from "../../common/types/AssetId.sol"; -import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {IGateway} from "../../common/interfaces/IGateway.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; import {IHub} from "../../hub/interfaces/IHub.sol"; import {IHubRegistry} from "../../hub/interfaces/IHubRegistry.sol"; diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index 9d5a1319e..c033b3140 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -6,7 +6,6 @@ import {INAVHook} from "./INAVManager.sol"; import {D18} from "../../../misc/types/D18.sol"; import {PoolId} from "../../../common/types/PoolId.sol"; -import {AssetId} from "../../../common/types/AssetId.sol"; import {ShareClassId} from "../../../common/types/ShareClassId.sol"; interface ISimplePriceManager is INAVHook { diff --git a/test/managers/hub/unit/SimplePriceManager.t.sol b/test/managers/hub/unit/SimplePriceManager.t.sol index f985f1d6c..cd6227187 100644 --- a/test/managers/hub/unit/SimplePriceManager.t.sol +++ b/test/managers/hub/unit/SimplePriceManager.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.28; -import {D18, d18} from "../../../../src/misc/types/D18.sol"; +import {d18} from "../../../../src/misc/types/D18.sol"; import {Multicall} from "../../../../src/misc/Multicall.sol"; import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; import {PoolId} from "../../../../src/common/types/PoolId.sol"; -import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; -import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; +import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; diff --git a/test/managers/spoke/unit/QueueManager.t.sol b/test/managers/spoke/unit/QueueManager.t.sol index 990e6accc..80b07892e 100644 --- a/test/managers/spoke/unit/QueueManager.t.sol +++ b/test/managers/spoke/unit/QueueManager.t.sol @@ -10,12 +10,11 @@ import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; import {ShareClassId} from "../../../../src/common/types/ShareClassId.sol"; import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; +import {QueueManager} from "../../../../src/managers/spoke/QueueManager.sol"; import {IBalanceSheet} from "../../../../src/spoke/interfaces/IBalanceSheet.sol"; import {IUpdateContract} from "../../../../src/spoke/interfaces/IUpdateContract.sol"; -import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; - -import {QueueManager} from "../../../../src/managers/spoke/QueueManager.sol"; import {IQueueManager} from "../../../../src/managers/spoke/interfaces/IQueueManager.sol"; +import {UpdateContractMessageLib} from "../../../../src/spoke/libraries/UpdateContractMessageLib.sol"; import "forge-std/Test.sol"; From 33102f6f725043277a307a18be04f605bf2988ab Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:56:20 +0200 Subject: [PATCH 82/83] remove networks from struct --- src/managers/hub/NAVManager.sol | 2 -- src/managers/hub/SimplePriceManager.sol | 15 ++++++++------- .../hub/interfaces/ISimplePriceManager.sol | 3 +-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/managers/hub/NAVManager.sol b/src/managers/hub/NAVManager.sol index d27720b1f..0eb98929c 100644 --- a/src/managers/hub/NAVManager.sol +++ b/src/managers/hub/NAVManager.sol @@ -58,7 +58,6 @@ contract NAVManager is INAVManager, Auth { /// @inheritdoc INAVManager function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { manager[poolId][manager_] = canManage; - emit UpdateManager(poolId, manager_, canManage); } @@ -146,7 +145,6 @@ contract NAVManager is INAVManager, Auth { require(address(navHook[poolId]) != address(0), InvalidNAVHook()); navHook[poolId].onTransfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); - emit Transfer(poolId, scId, fromCentrifugeId, toCentrifugeId, sharesTransferred); } diff --git a/src/managers/hub/SimplePriceManager.sol b/src/managers/hub/SimplePriceManager.sol index 045981964..dc672b64d 100644 --- a/src/managers/hub/SimplePriceManager.sol +++ b/src/managers/hub/SimplePriceManager.sol @@ -25,6 +25,7 @@ contract SimplePriceManager is ISimplePriceManager, Auth { mapping(PoolId poolId => Metrics) public metrics; mapping(PoolId poolId => mapping(address => bool)) public manager; mapping(PoolId poolId => mapping(uint16 centrifugeId => NetworkMetrics)) public networkMetrics; + mapping(PoolId poolId => uint16[]) internal _networks; constructor(IHub hub_, address deployer) Auth(deployer) { hub = hub_; @@ -56,20 +57,20 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc ISimplePriceManager function networks(PoolId poolId) external view returns (uint16[] memory) { - return metrics[poolId].networks; + return _networks[poolId]; } /// @inheritdoc ISimplePriceManager function addNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { require(shareClassManager.shareClassCount(poolId) == 1, InvalidShareClassCount()); - metrics[poolId].networks.push(centrifugeId); - emit UpdateNetworks(poolId, metrics[poolId].networks); + _networks[poolId].push(centrifugeId); + emit UpdateNetworks(poolId, _networks[poolId]); } /// @inheritdoc ISimplePriceManager function removeNetwork(PoolId poolId, uint16 centrifugeId) external onlyHubManager(poolId) { - uint16[] storage networks_ = metrics[poolId].networks; + uint16[] storage networks_ = _networks[poolId]; uint256 length = networks_.length; for (uint256 i; i < length; i++) { if (networks_[i] == centrifugeId) { @@ -94,7 +95,6 @@ contract SimplePriceManager is ISimplePriceManager, Auth { /// @inheritdoc ISimplePriceManager function updateManager(PoolId poolId, address manager_, bool canManage) external onlyHubManager(poolId) { manager[poolId][manager_] = canManage; - emit UpdateManager(poolId, manager_, canManage); } @@ -132,11 +132,12 @@ contract SimplePriceManager is ISimplePriceManager, Auth { networkMetrics_.netAssetValue = netAssetValue; networkMetrics_.issuance = issuance; - uint256 networkCount = metrics_.networks.length; + uint16[] storage networks_ = _networks[poolId]; + uint256 networkCount = networks_.length; hub.updateSharePrice(poolId, scId, price); for (uint256 i; i < networkCount; i++) { - hub.notifySharePrice(poolId, scId, metrics_.networks[i], address(0)); + hub.notifySharePrice(poolId, scId, networks_[i], address(0)); } emit Update(poolId, scId, metrics_.netAssetValue, metrics_.issuance, price); diff --git a/src/managers/hub/interfaces/ISimplePriceManager.sol b/src/managers/hub/interfaces/ISimplePriceManager.sol index c033b3140..4e05e7dcd 100644 --- a/src/managers/hub/interfaces/ISimplePriceManager.sol +++ b/src/managers/hub/interfaces/ISimplePriceManager.sol @@ -30,7 +30,6 @@ interface ISimplePriceManager is INAVHook { struct Metrics { uint128 netAssetValue; uint128 issuance; - uint16[] networks; } struct NetworkMetrics { @@ -41,7 +40,7 @@ interface ISimplePriceManager is INAVHook { } function metrics(PoolId poolId) external view returns (uint128 netAssetValue, uint128 issuance); - function networks(PoolId poolId) external view returns (uint16[] memory networks); + function networks(PoolId poolId) external view returns (uint16[] memory); function networkMetrics(PoolId poolId, uint16 centrifugeId) external view From 46e0c408675c2e78636623b967b68631aa6e458f Mon Sep 17 00:00:00 2001 From: Onno Visser <23527729+onnovisser@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:05:48 +0200 Subject: [PATCH 83/83] re-add BatchSimplePriceManager --- src/managers/hub/BatchSimplePriceManager.sol | 194 +++++++++ .../interfaces/IBatchSimplePriceManager.sol | 72 +++ .../hub/unit/BatchSimplePriceManager.t.sol | 412 ++++++++++++++++++ 3 files changed, 678 insertions(+) create mode 100644 src/managers/hub/BatchSimplePriceManager.sol create mode 100644 src/managers/hub/interfaces/IBatchSimplePriceManager.sol create mode 100644 test/managers/hub/unit/BatchSimplePriceManager.t.sol diff --git a/src/managers/hub/BatchSimplePriceManager.sol b/src/managers/hub/BatchSimplePriceManager.sol new file mode 100644 index 000000000..154dc43c0 --- /dev/null +++ b/src/managers/hub/BatchSimplePriceManager.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {INAVHook} from "./interfaces/INAVManager.sol"; +import {SimplePriceManager} from "./SimplePriceManager.sol"; +import {IBatchSimplePriceManager} from "./interfaces/IBatchSimplePriceManager.sol"; + +import {D18, d18} from "../../misc/types/D18.sol"; + +import {PoolId} from "../../common/types/PoolId.sol"; +import {AssetId} from "../../common/types/AssetId.sol"; +import {ShareClassId} from "../../common/types/ShareClassId.sol"; +import {IHub} from "../../hub/interfaces/IHub.sol"; + +import {IBatchRequestManager} from "../../vaults/interfaces/IBatchRequestManager.sol"; + +/// @notice Simple price manager for single share class pools with async request management. +contract BatchSimplePriceManager is SimplePriceManager, IBatchSimplePriceManager { + constructor(IHub hub_, address deployer) SimplePriceManager(hub_, deployer) {} + + modifier onlyGateway() { + require(msg.sender == address(gateway), NotAuthorized()); + _; + } + + //---------------------------------------------------------------------------------------------- + // Updates + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc SimplePriceManager + function onUpdate(PoolId poolId, ShareClassId scId, uint16 centrifugeId, uint128 netAssetValue) + public + override(SimplePriceManager, INAVHook) + auth + { + NetworkMetrics memory networkMetrics_ = networkMetrics[poolId][centrifugeId]; + + // If there are pending epochs to be issued or revoked, skip updating the share price, as it will likely be off + if (networkMetrics_.issueEpochsBehind > 0 || networkMetrics_.revokeEpochsBehind > 0) return; + + super.onUpdate(poolId, scId, centrifugeId, netAssetValue); + } + + //---------------------------------------------------------------------------------------------- + // Manager actions + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc IBatchSimplePriceManager + function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) + external payable + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + + networkMetrics_.issueEpochsBehind++; + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); + requestManager.approveDeposits{value: msg.value}( + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset, msg.sender + ); + } + + /// @inheritdoc IBatchSimplePriceManager + function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) + external payable + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][depositAssetId.centrifugeId()]; + + require(networkMetrics_.issueEpochsBehind > 0, MismatchedEpochs()); + networkMetrics_.issueEpochsBehind--; + + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.issueShares{value: msg.value}(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit, msg.sender); + } + + /// @inheritdoc IBatchSimplePriceManager + function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) + external + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; + + networkMetrics_.revokeEpochsBehind++; + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); + requestManager.approveRedeems( + poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset + ); + } + + /// @inheritdoc IBatchSimplePriceManager + function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) + external payable + onlyManager(poolId) + { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); + + NetworkMetrics storage networkMetrics_ = networkMetrics[poolId][payoutAssetId.centrifugeId()]; + + require(networkMetrics_.revokeEpochsBehind > 0, MismatchedEpochs()); + networkMetrics_.revokeEpochsBehind--; + + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.revokeShares{value: msg.value}(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit, msg.sender); + } + + /// @inheritdoc IBatchSimplePriceManager + function approveDepositsAndIssueShares( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external payable onlyManager(poolId) { + gateway.withBatch{value: msg.value}( + abi.encodeWithSelector( + BatchSimplePriceManager.approveDepositsAndIssueSharesCallback.selector, + poolId, + scId, + depositAssetId, + approvedAssetAmount, + extraGasLimit + ), + msg.sender + ); + } + + + function approveDepositsAndIssueSharesCallback( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external onlyGateway { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, depositAssetId.centrifugeId()))); + uint32 nowDepositEpochId = requestManager.nowDepositEpoch(scId, depositAssetId); + uint32 nowIssueEpochId = requestManager.nowIssueEpoch(scId, depositAssetId); + + require(nowDepositEpochId == nowIssueEpochId, MismatchedEpochs()); + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, depositAssetId); + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.approveDeposits( + poolId, scId, depositAssetId, nowDepositEpochId, approvedAssetAmount, pricePoolPerAsset, address(0) + ); + requestManager.issueShares(poolId, scId, depositAssetId, nowIssueEpochId, navPoolPerShare, extraGasLimit, address(0)); + } + + /// @inheritdoc IBatchSimplePriceManager + function approveRedeemsAndRevokeShares( + PoolId poolId, + ShareClassId scId, + AssetId payoutAssetId, + uint128 approvedShareAmount, + uint128 extraGasLimit + ) external payable onlyManager(poolId) { + require(scId.index() == 1, InvalidShareClass()); + IBatchRequestManager requestManager = + IBatchRequestManager(address(hubRegistry.hubRequestManager(poolId, payoutAssetId.centrifugeId()))); + uint32 nowRedeemEpochId = requestManager.nowRedeemEpoch(scId, payoutAssetId); + uint32 nowRevokeEpochId = requestManager.nowRevokeEpoch(scId, payoutAssetId); + + require(nowRedeemEpochId == nowRevokeEpochId, MismatchedEpochs()); + + D18 pricePoolPerAsset = hub.pricePoolPerAsset(poolId, scId, payoutAssetId); + D18 navPoolPerShare = _navPerShare(poolId); + requestManager.approveRedeems( + poolId, scId, payoutAssetId, nowRedeemEpochId, approvedShareAmount, pricePoolPerAsset + ); + requestManager.revokeShares{value: msg.value}(poolId, scId, payoutAssetId, nowRevokeEpochId, navPoolPerShare, extraGasLimit, msg.sender); + } +} diff --git a/src/managers/hub/interfaces/IBatchSimplePriceManager.sol b/src/managers/hub/interfaces/IBatchSimplePriceManager.sol new file mode 100644 index 000000000..1497575be --- /dev/null +++ b/src/managers/hub/interfaces/IBatchSimplePriceManager.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {ISimplePriceManager} from "./ISimplePriceManager.sol"; + +import {PoolId} from "../../../common/types/PoolId.sol"; +import {AssetId} from "../../../common/types/AssetId.sol"; +import {ShareClassId} from "../../../common/types/ShareClassId.sol"; + +interface IBatchSimplePriceManager is ISimplePriceManager { + //---------------------------------------------------------------------------------------------- + // Manager actions + //---------------------------------------------------------------------------------------------- + + /// @notice Approve deposit requests for a given asset amount + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param approvedAssetAmount Amount of assets to approve for deposit + function approveDeposits(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 approvedAssetAmount) + external payable; + + /// @notice Issue shares for approved deposit epochs + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function issueShares(PoolId poolId, ShareClassId scId, AssetId depositAssetId, uint128 extraGasLimit) external payable; + + /// @notice Approve redemption requests for a given share amount + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param approvedShareAmount Amount of shares to approve for redemption + function approveRedeems(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 approvedShareAmount) + external; + + /// @notice Revoke shares from approved redemption requests + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function revokeShares(PoolId poolId, ShareClassId scId, AssetId payoutAssetId, uint128 extraGasLimit) external payable; + + /// @notice Approve deposits and issue shares in sequence using current NAV per share + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param depositAssetId The asset ID for deposits + /// @param approvedAssetAmount Amount of assets to approve + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function approveDepositsAndIssueShares( + PoolId poolId, + ShareClassId scId, + AssetId depositAssetId, + uint128 approvedAssetAmount, + uint128 extraGasLimit + ) external payable; + + /// @notice Approve redeems and revoke shares in sequence using current NAV per share + /// @param poolId The pool ID + /// @param scId The share class ID + /// @param payoutAssetId The asset ID for payouts + /// @param approvedShareAmount Amount of shares to approve for redemption + /// @param extraGasLimit Extra gas limit for some computation that may need to happen on the remote chain + function approveRedeemsAndRevokeShares( + PoolId poolId, + ShareClassId scId, + AssetId payoutAssetId, + uint128 approvedShareAmount, + uint128 extraGasLimit + ) external payable; +} diff --git a/test/managers/hub/unit/BatchSimplePriceManager.t.sol b/test/managers/hub/unit/BatchSimplePriceManager.t.sol new file mode 100644 index 000000000..635fec74d --- /dev/null +++ b/test/managers/hub/unit/BatchSimplePriceManager.t.sol @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {D18, d18} from "../../../../src/misc/types/D18.sol"; +import {Multicall} from "../../../../src/misc/Multicall.sol"; +import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; + +import {PoolId} from "../../../../src/common/types/PoolId.sol"; +import {IBatchedMulticall} from "../../../../src/common/interfaces/IBatchedMulticall.sol"; +import {IGateway} from "../../../../src/common/interfaces/IGateway.sol"; +import {AssetId, newAssetId} from "../../../../src/common/types/AssetId.sol"; +import {ShareClassId, newShareClassId} from "../../../../src/common/types/ShareClassId.sol"; + +import {IHub} from "../../../../src/hub/interfaces/IHub.sol"; +import {IHubRegistry} from "../../../../src/hub/interfaces/IHubRegistry.sol"; +import {BatchSimplePriceManager} from "../../../../src/managers/hub/BatchSimplePriceManager.sol"; +import {IShareClassManager} from "../../../../src/hub/interfaces/IShareClassManager.sol"; +import {IBatchSimplePriceManager} from "../../../../src/managers/hub/interfaces/IBatchSimplePriceManager.sol"; +import {ISimplePriceManager} from "../../../../src/managers/hub/interfaces/ISimplePriceManager.sol"; + +import {IBatchRequestManager} from "../../../../src/vaults/interfaces/IBatchRequestManager.sol"; + +import "forge-std/Test.sol"; + +contract IsContract {} + +contract MockGateway { + function withBatch(bytes memory data, address) external payable returns (uint256 cost) { + (bool success, bytes memory returnData) = msg.sender.call(data); + if (!success) { + uint256 length = returnData.length; + require(length != 0, "Empty revert"); + + assembly ("memory-safe") { + revert(add(32, returnData), length) + } + } + return 0; + } +} + +contract MockHub is Multicall { + function notifySharePrice(PoolId poolId, ShareClassId scId, uint16 centrifugeId) external payable {} +} + +contract BatchSimplePriceManagerTest is Test { + PoolId constant POOL_A = PoolId.wrap(1); + ShareClassId immutable SC_1 = newShareClassId(POOL_A, 1); + ShareClassId immutable SC_2 = newShareClassId(POOL_A, 2); + uint16 constant CENTRIFUGE_ID_1 = 1; + + AssetId asset1 = newAssetId(1, 1); + + address hub = address(new MockHub()); + address gateway = address(new MockGateway()); + address hubRegistry = address(new IsContract()); + address shareClassManager = address(new IsContract()); + address batchRequestManager = address(new IsContract()); + + address unauthorized = makeAddr("unauthorized"); + address hubManager = makeAddr("hubManager"); + address manager = makeAddr("manager"); + address caller = makeAddr("caller"); + address auth = makeAddr("auth"); + + BatchSimplePriceManager priceManager; + + function setUp() public virtual { + _setupMocks(); + _deployManager(); + } + + function _setupMocks() internal { + vm.mockCall(hub, abi.encodeWithSelector(IHub.shareClassManager.selector), abi.encode(shareClassManager)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.hubRegistry.selector), abi.encode(hubRegistry)); + vm.mockCall(hub, abi.encodeWithSelector(IBatchedMulticall.gateway.selector), abi.encode(gateway)); + vm.mockCall(hub, abi.encodeWithSelector(IHub.updateSharePrice.selector), abi.encode()); + vm.mockCall(hub, abi.encodeWithSelector(IHub.notifySharePrice.selector), abi.encode(uint256(0))); + vm.mockCall( + hub, abi.encodeWithSelector(IHub.pricePoolPerAsset.selector, POOL_A, SC_1, asset1), abi.encode(d18(1, 1)) + ); + + vm.mockCall( + hubRegistry, + abi.encodeWithSelector(IHubRegistry.hubRequestManager.selector), + abi.encode(batchRequestManager) + ); + vm.mockCall(hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector), abi.encode(false)); + vm.mockCall( + hubRegistry, abi.encodeWithSelector(IHubRegistry.manager.selector, POOL_A, hubManager), abi.encode(true) + ); + + vm.mockCall( + shareClassManager, + abi.encodeWithSelector(IShareClassManager.issuance.selector, SC_1, CENTRIFUGE_ID_1), + abi.encode(100) + ); + + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.approveDeposits.selector), + abi.encode(uint256(0)) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.issueShares.selector), + abi.encode(uint256(0)) + ); + vm.mockCall( + batchRequestManager, abi.encodeWithSelector(IBatchRequestManager.approveRedeems.selector), abi.encode() + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.revokeShares.selector), + abi.encode(uint256(0)) + ); + } + + function _deployManager() internal { + priceManager = new BatchSimplePriceManager(IHub(hub), auth); + vm.prank(auth); + priceManager.rely(caller); + + vm.prank(hubManager); + priceManager.updateManager(POOL_A, manager, true); + + vm.deal(address(priceManager), 1 ether); + } +} + +contract BatchSimplePriceManagerInvestorActionsTest is BatchSimplePriceManagerTest { + D18 expectedNavPerShare = d18(10, 1); // 1000/100 = 10 + + function setUp() public override { + super.setUp(); + + vm.prank(caller); + priceManager.onUpdate(POOL_A, SC_1, CENTRIFUGE_ID_1, 1000); + } + + function testApproveDepositsAndIssueSharesSuccess() public { + uint128 approvedAssetAmount = 500; + uint128 extraGasLimit = 100000; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveDeposits.selector, POOL_A, SC_1, asset1, 1, approvedAssetAmount, d18(1, 1) + ) + ); + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.issueShares.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + expectedNavPerShare, + extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, approvedAssetAmount, extraGasLimit); + } + + function testApproveDepositsAndIssueSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); + } + + function testApproveDepositsAndIssueSharesMismatchedEpochs() public { + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowDepositEpoch.selector, SC_1, asset1), + abi.encode(1) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowIssueEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_1, asset1, 500, 100000); + } + + function testApproveRedeemsAndRevokeSharesSuccess() public { + uint128 approvedShareAmount = 50; + uint128 extraGasLimit = 100000; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveRedeems.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + approvedShareAmount, + d18(1, 1) + ) + ); + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.revokeShares.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + expectedNavPerShare, + extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, approvedShareAmount, extraGasLimit); + } + + function testApproveRedeemsAndRevokeSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); + } + + function testApproveRedeemsAndRevokeSharesMismatchedEpochs() public { + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRedeemEpoch.selector, SC_1, asset1), + abi.encode(2) + ); + vm.mockCall( + batchRequestManager, + abi.encodeWithSelector(IBatchRequestManager.nowRevokeEpoch.selector, SC_1, asset1), + abi.encode(3) + ); + + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_1, asset1, 50, 100000); + } + + function testApproveRedeemsSuccess() public { + uint128 approvedShareAmount = 50; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveRedeems.selector, + POOL_A, + SC_1, + asset1, + uint32(2), + approvedShareAmount, + d18(1, 1) + ) + ); + + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_1, asset1, approvedShareAmount); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(revokeEpochsBehind, 1); + assertEq(issueEpochsBehind, 0); + } + + function testApproveRedeemsUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); + } + + function testRevokeSharesSuccess() public { + uint128 extraGasLimit = 100000; + + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_1, asset1, 50); + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.revokeShares.selector, POOL_A, SC_1, asset1, uint32(2), d18(10, 1), extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_1, asset1, extraGasLimit); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(revokeEpochsBehind, 0); + assertEq(issueEpochsBehind, 0); + } + + function testRevokeSharesUnauthorized() public { + vm.expectRevert(IAuth.NotAuthorized.selector); + vm.prank(unauthorized); + priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); + } + + function testRevokeSharesWithoutPendingEpochs() public { + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_1, asset1, 100000); + } + + function testApproveDepositsSuccess() public { + uint128 approvedAssetAmount = 500; + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.approveDeposits.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + approvedAssetAmount, + d18(1, 1) + ) + ); + + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_1, asset1, approvedAssetAmount); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(issueEpochsBehind, 1); + assertEq(revokeEpochsBehind, 0); + } + + function testIssueSharesSuccess() public { + uint128 extraGasLimit = 100000; + + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_1, asset1, 500); + + vm.expectCall( + address(batchRequestManager), + abi.encodeWithSelector( + IBatchRequestManager.issueShares.selector, + POOL_A, + SC_1, + asset1, + uint32(1), + expectedNavPerShare, + extraGasLimit + ) + ); + + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_1, asset1, extraGasLimit); + + (,, uint32 issueEpochsBehind, uint32 revokeEpochsBehind) = priceManager.networkMetrics(POOL_A, CENTRIFUGE_ID_1); + assertEq(issueEpochsBehind, 0); + assertEq(revokeEpochsBehind, 0); + } + + function testIssueSharesWithoutPendingEpochs() public { + vm.expectRevert(ISimplePriceManager.MismatchedEpochs.selector); + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_1, asset1, 100000); + } + + function testInvalidShareClass() public { + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveDeposits(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.issueShares(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveRedeems(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.revokeShares(POOL_A, SC_2, asset1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveDepositsAndIssueShares(POOL_A, SC_2, asset1, 1, 1); + + vm.expectRevert(ISimplePriceManager.InvalidShareClass.selector); + vm.prank(manager); + priceManager.approveRedeemsAndRevokeShares(POOL_A, SC_2, asset1, 1, 1); + } +}