Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions contracts/docs/contracts-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,14 @@ Key functions:
Important notes:

- `BondManager` depends on privileged role setup across `BondAuction`,
`BondToken`, `BondDvP`, and `Wnok`.
`BondToken`, `BondDvP`, and `Wnok`. The constructor rejects a zero address
for any wired contract, the controller, or the reserve.
- The deploy scripts give `BondManager` `BOND_CONTROLLER_ROLE` on `BondToken`
but no ERC-1410 controller status: it runs the lifecycle and moves units only
through `BondDvP`. Once wiring is done, the deployer key hands
`DEFAULT_ADMIN_ROLE` on `BondManager`, `BondAuction`, `BondToken`, and
`BondDvP` (and `BOND_ADMIN_ROLE` on `BondToken`) to the Norges Bank owner key
and renounces its own.
- Every cash leg settles in `Wnok` against one government reserve account
(`GOV_RESERVE`, fixed at deployment): issuance credits the reserve, while
buyback and coupon payments debit it. The reserve must hold enough WNOK and
Expand Down Expand Up @@ -238,6 +245,12 @@ Key functions:
marks the partition matured after the final coupon payment.
- `isinToPartition(isin)`, `partitionToIsin(partition)`, `getCouponDetails(isin)`:
useful lookup helpers for integrations and operational tooling.
- `addController(controller)` and `removeController(controller)`
(`BOND_ADMIN_ROLE`):
manage ERC-1410 controllers, which act as operators for every holder. They
grant or revoke no lifecycle rights; `BOND_CONTROLLER_ROLE` is granted
separately. The deploy scripts make only `BondDvP` a controller, since it
moves units during settlement.

Important notes:

Expand Down Expand Up @@ -352,14 +365,18 @@ Key functions:
- `initializeSellOrders(numIssuance, price, secContrAddr, tbdContrAddr, investorSecAddr, investorTbdAddr)`:
seeds initial sell-side book state.
- `revokeBuyOrder(orderId)` and `revokeSellOrder(orderId)`:
cancel open orders.
cancel open orders; only the broker that submitted the order may revoke it.
- `getBuyOrders()`, `getSellOrders()`, `getAllBuyOrders()`,
`getAllSellOrders()`:
`getAllSellOrders()`, `getOrder(orderId)`:
inspect open book state.

Important notes:

- The order book is tightly coupled to the settlement model in `DvP`.
- `SUBMIT_ORDER_ROLE` is granted only to the `Broker` contracts, which submit
orders on behalf of registered clients. A `Broker` lets a client revoke only
orders placed for that client's own securities wallet
(`OrderNotOwnedByClient` otherwise).
- Unknown settlement errors are intentionally treated differently from
buyer-side or seller-side faults so matching can continue where appropriate.
A maker that fails with an unknown reason stays in the book; once a price
Expand Down
6 changes: 4 additions & 2 deletions contracts/docs/contracts-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ in [`contracts/src/common/Roles.sol`](../src/common/Roles.sol).

| Role | Used by | Why it matters |
| --- | --- | --- |
| `DEFAULT_ADMIN_ROLE` | multiple contracts | Can grant and revoke other roles. This is the highest-trust role and should be tightly controlled. |
| `DEFAULT_ADMIN_ROLE` | multiple contracts | Can grant and revoke other roles. This is the highest-trust role and should be tightly controlled. On `BondManager`, `BondAuction`, `BondToken`, and `BondDvP` the deploy scripts hand it to the Norges Bank owner key and the deployer key renounces it. |
| `BOND_ADMIN_ROLE` | `BondToken` | Can add and remove ERC-1410 controllers, which act as operators over every holder's units. Only `BondDvP` is made a controller, because it moves units during settlement. |
| `BOND_MANAGER_ROLE` | `BondManager` | Can open, close, cancel, and finalise auctions, recover failed issuance, pay coupons, and redeem bonds. |
| `BOND_AUCTION_ADMIN_ROLE` | `BondAuction` | Can create, close, cancel, and finalise auctions. In practice this should align with the `BondManager` control path. |
| `BOND_CONTROLLER_ROLE` | `BondToken` | Can create partitions, extend or reduce offering, mint by ISIN, update coupon state, and mark bonds matured. |
| `BOND_CONTROLLER_ROLE` | `BondToken` | Can create partitions, extend or reduce offering, mint by ISIN, update coupon state, and mark bonds matured. Held by `BondManager` and by `BondDvP` (for redemption and buyback burns); granted separately from controller status. |
| `SUBMIT_ORDER_ROLE` | `OrderBook` | Can submit buy and sell orders. Granted only to the `Broker` contracts, which let a client revoke only its own orders. |
| `SETTLE_ROLE` | `BondDvP` | Can execute settlement, including bond-leg and cash-leg transfers. Misconfiguration here directly affects issuance, buyback, coupon, and redemption flows. |
| `MINTER_ROLE` / `BURNER_ROLE` / `TRANSFER_FROM_ROLE` | `Wnok` and related cash flows | Control the tokenized cash leg and the ability to move cash during settlement. `BondDvP` holds `TRANSFER_FROM_ROLE` and moves WNOK for every bond cash leg, bounded by the payer's allowance: the bidder's for issuance and the government reserve account's for buyback, coupon, and redemption. |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ function sell(address secContrAddr, uint256 amount, uint256 askPrice) public ret

Revokes a buy order.

The caller must be a registered client. Wallet addresses are resolved from ClientList.
The caller must be a registered client and the order must belong to the caller's
securities wallet. Wallet addresses are resolved from ClientList.


```solidity
Expand All @@ -146,7 +147,8 @@ function revokeBuyOrder(bytes32 orderId) public returns (bool);

Revokes a sell order.

The caller must be a registered client. Wallet addresses are resolved from ClientList.
The caller must be a registered client and the order must belong to the caller's
securities wallet. Wallet addresses are resolved from ClientList.


```solidity
Expand Down Expand Up @@ -199,3 +201,14 @@ function getBuyOrders() public view returns (IOrderBook.Order[] memory);
|`<none>`|`IOrderBook.Order[]`|Order[] -> An array of the caller's active buy orders from the order book.|


### _requireOwnOrder

Reverts unless an existing order was placed for the caller's securities wallet.
An unknown id is left to the OrderBook, which reverts with OrderNotFound.


```solidity
function _requireOwnOrder(bytes32 orderId) internal view;
```


12 changes: 12 additions & 0 deletions contracts/docs/natspec/src/src/common/Errors.sol/library.Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ error MissingRole(bytes32 role, address account);
error NotInAllowlist(string list, address addr);
```

### OrderNotOwnedByClient

```solidity
error OrderNotOwnedByClient(bytes32 orderId);
```

### BankAddressZero

```solidity
Expand Down Expand Up @@ -237,6 +243,12 @@ error CctFromNotCaller(address from, address caller);
error BondTokenAddressZero();
```

### BondAuctionAddressZero

```solidity
error BondAuctionAddressZero();
```

### DurationScalarZero

```solidity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,15 @@ function getSellOrders(address investorSecAddr) external view override returns (
function getAllBuyOrders() external view override returns (Order[] memory);
```

### getOrder

Returns one order by id; every field is zero when the order does not exist.


```solidity
function getOrder(bytes32 orderId) external view override returns (Order memory);
```

### getAllSellOrders


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ function getAllBuyOrders() external view returns (Order[] memory);
function getAllSellOrders() external view returns (Order[] memory);
```

### getOrder

Returns one order by id; every field is zero when the order does not exist.


```solidity
function getOrder(bytes32 orderId) external view returns (Order memory);
```

### revokeBuyOrder


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,11 @@ constructor(string memory _name, string memory _symbol)

### addController

Add new controller
Add an ERC-1410 controller

To add dedicated manager role
A controller is an operator for every holder, so it can move any holder's units
through `operatorTransferByPartition`. This grants no lifecycle rights; those come
from BOND_CONTROLLER_ROLE, granted separately with `grantRole`.


```solidity
Expand All @@ -149,7 +151,24 @@ function addController(address _controller) external onlyRole(Roles.BOND_ADMIN_R

|Name|Type|Description|
|----|----|-----------|
|`_controller`|`address`|of new controller|
|`_controller`|`address`|Address of the new controller|


### removeController

Remove an ERC-1410 controller

Leaves BOND_CONTROLLER_ROLE untouched; a no-op when the address is not a controller.


```solidity
function removeController(address _controller) external onlyRole(Roles.BOND_ADMIN_ROLE);
```
**Parameters**

|Name|Type|Description|
|----|----|-----------|
|`_controller`|`address`|Address of the controller to remove|


### supportsInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ function isMatured(bytes32 partition) external view returns (bool);
function addController(address _controller) external;
```

### removeController


```solidity
function removeController(address _controller) external;
```

### isinToPartition


Expand Down
10 changes: 2 additions & 8 deletions contracts/script/csd/06_OrderBook.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@ contract OrderBookScript is RegistryScript {
function run() external {
uint256 deployerKey = vm.envUint("PK_DEPLOYER");
uint256 ownerKey = vm.envUint("PK_NORGES_BANK");
uint256 csdKey = vm.envUint("PK_CSD");
address owner = vm.addr(ownerKey);

address csdAddr = vm.addr(vm.envUint("PK_CSD"));
address broker1Addr = vm.addr(vm.envUint("PK_BROKER1"));
address broker2Addr = vm.addr(vm.envUint("PK_BROKER2"));

address registryAddr = vm.envAddress("REGISTRY_ADDR");
_ensureRegistry(registryAddr, owner);
Expand All @@ -30,15 +27,12 @@ contract OrderBookScript is RegistryScript {
(bool found, address stockTokenAddr) = stockTokenFactory.getDeployedStockToken("NO0001234567");
require(found, "Stock token NO0001234567 not found.");

// SUBMIT_ORDER_ROLE goes only to the broker contracts (09_BrokersSetup), which submit
// orders on behalf of their registered clients; broker EOAs get no direct order access.
vm.startBroadcast(deployerKey);
OrderBook orderBook = new OrderBook(csdAddr, wnokAddr, address(dvp), stockTokenAddr);
vm.stopBroadcast();

vm.startBroadcast(csdKey);
orderBook.grantRole(Roles.SUBMIT_ORDER_ROLE, broker1Addr);
orderBook.grantRole(Roles.SUBMIT_ORDER_ROLE, broker2Addr);
vm.stopBroadcast();

vm.startBroadcast(ownerKey);
dvp.grantRole(Roles.SETTLE_ROLE, address(orderBook));
registry.setContract(vm.envString("ORDERBOOK_CONTRACT_NAME"), address(orderBook));
Expand Down
43 changes: 42 additions & 1 deletion contracts/script/norges-bank/11_BondSetup.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {BondDvP} from "@norges-bank/BondDvP.sol";
import {Wnok} from "@norges-bank/Wnok.sol";

import {Roles} from "@common/Roles.sol";
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";

contract BondSetupScript is RegistryScript {
function setUp() public {}
Expand Down Expand Up @@ -45,8 +46,13 @@ contract BondSetupScript is RegistryScript {
vm.startBroadcast(deployerKey);

bondAuction.grantRole(Roles.BOND_AUCTION_ADMIN_ROLE, bondManagerAddr);
bondToken.addController(bondManagerAddr);

// BondManager runs the bond lifecycle but never moves holder units itself, so it gets
// BOND_CONTROLLER_ROLE only. BondDvP moves units in settlement (ERC-1410 controller)
// and burns them on redemption and buyback (BOND_CONTROLLER_ROLE).
bondToken.grantRole(Roles.BOND_CONTROLLER_ROLE, bondManagerAddr);
bondToken.addController(address(bondDvp));
bondToken.grantRole(Roles.BOND_CONTROLLER_ROLE, address(bondDvp));

bondDvp.grantRole(Roles.SETTLE_ROLE, bondManagerAddr);

Expand Down Expand Up @@ -79,5 +85,40 @@ contract BondSetupScript is RegistryScript {
vm.startBroadcast(dnbKey); // DNB
wnok.approve(address(bondDvp), type(uint256).max);
vm.stopBroadcast();

_handAdminToOwner(deployerKey, owner, bondManagerAddr, bondAuction, bondToken, bondDvp);
}

/**
* @dev The deployer key is only needed to deploy and wire the bond contracts. Once wiring is
* done, the Norges Bank owner key takes over DEFAULT_ADMIN_ROLE (and BOND_ADMIN_ROLE on
* BondToken) so roles can still be rotated, and the deployer renounces its own.
*/
function _handAdminToOwner(
uint256 deployerKey,
address owner,
address bondManagerAddr,
BondAuction bondAuction,
BondToken bondToken,
BondDvP bondDvp
) internal {
address deployer = vm.addr(deployerKey);
if (deployer == owner) return;

IAccessControl[4] memory bondContracts = [
IAccessControl(bondManagerAddr),
IAccessControl(address(bondAuction)),
IAccessControl(address(bondToken)),
IAccessControl(address(bondDvp))
];

vm.startBroadcast(deployerKey);
bondToken.grantRole(Roles.BOND_ADMIN_ROLE, owner);
bondToken.renounceRole(Roles.BOND_ADMIN_ROLE, deployer);
for (uint256 i = 0; i < bondContracts.length; i++) {
bondContracts[i].grantRole(Roles.DEFAULT_ADMIN_ROLE, owner);
bondContracts[i].renounceRole(Roles.DEFAULT_ADMIN_ROLE, deployer);
}
vm.stopBroadcast();
}
}
19 changes: 17 additions & 2 deletions contracts/src/broker/Broker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,23 +97,27 @@ contract Broker is ClientList {

/**
* @notice Revokes a buy order.
* @dev The caller must be a registered client. Wallet addresses are resolved from ClientList.
* @dev The caller must be a registered client and the order must belong to the caller's
* securities wallet. Wallet addresses are resolved from ClientList.
* @param orderId The unique identifier of the Order.
* @return true/false representing the success of the transaction.
*/
function revokeBuyOrder(bytes32 orderId) public returns (bool) {
clientExistsGuard(msg.sender);
_requireOwnOrder(orderId);
return _ORDER_BOOK.revokeBuyOrder(orderId);
}

/**
* @notice Revokes a sell order.
* @dev The caller must be a registered client. Wallet addresses are resolved from ClientList.
* @dev The caller must be a registered client and the order must belong to the caller's
* securities wallet. Wallet addresses are resolved from ClientList.
* @param orderId The unique identifier of the Order.
* @return true/false representing the success of the transaction.
*/
function revokeSellOrder(bytes32 orderId) public returns (bool) {
clientExistsGuard(msg.sender);
_requireOwnOrder(orderId);
return _ORDER_BOOK.revokeSellOrder(orderId);
}

Expand All @@ -136,4 +140,15 @@ contract Broker is ClientList {
clientExistsGuard(msg.sender);
return _ORDER_BOOK.getBuyOrders(getSecuritiesWallet(msg.sender));
}

/**
* @dev Reverts unless an existing order was placed for the caller's securities wallet.
* An unknown id is left to the OrderBook, which reverts with OrderNotFound.
*/
function _requireOwnOrder(bytes32 orderId) internal view {
IOrderBook.Order memory order = _ORDER_BOOK.getOrder(orderId);
if (order.id != bytes32(0) && order.investorSecAddr != getSecuritiesWallet(msg.sender)) {
revert Errors.OrderNotOwnedByClient(orderId);
}
}
}
4 changes: 4 additions & 0 deletions contracts/src/common/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ library Errors {
error MissingRole(bytes32 role, address account);
error NotInAllowlist(string list, address addr);

// --- Broker ---
error OrderNotOwnedByClient(bytes32 orderId);

// --- Wnok ---
error BankAddressZero();
error CallbackFailed(bytes4 received);
Expand All @@ -54,6 +57,7 @@ library Errors {

// --- BondManager ---
error BondTokenAddressZero();
error BondAuctionAddressZero();
error DurationScalarZero();
error BondUnitNominalZero();
error BondDoesNotExist(string isin);
Expand Down
7 changes: 7 additions & 0 deletions contracts/src/csd/OrderBook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,13 @@ contract OrderBook is IOrderBook, AccessControl, ReentrancyGuard {
return _fetchOrders(true, address(0), address(0));
}

/**
* @inheritdoc IOrderBook
*/
function getOrder(bytes32 orderId) external view override returns (Order memory) {
return orders[orderId];
}

function getAllSellOrders() external view override returns (Order[] memory) {
return _fetchOrders(false, address(0), address(0));
}
Expand Down
5 changes: 5 additions & 0 deletions contracts/src/interfaces/IOrderBook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ interface IOrderBook {
function getAllBuyOrders() external view returns (Order[] memory);
function getAllSellOrders() external view returns (Order[] memory);

/**
* @notice Returns one order by id; every field is zero when the order does not exist.
*/
function getOrder(bytes32 orderId) external view returns (Order memory);

function revokeBuyOrder(bytes32 orderId) external returns (bool);
function revokeSellOrder(bytes32 orderId) external returns (bool);

Expand Down
Loading
Loading