Skip to content

Commit

Permalink
Add allowlist role to swap endpoint for sanctions compliance (#725)
Browse files Browse the repository at this point in the history
* whole thing

* dont forget the approval endpoint

* role rename, assignd in initializer, snapshot, docgen

---------

Co-authored-by: amiecorso <[email protected]>
  • Loading branch information
amiecorso and amiecorso authored Nov 14, 2023
1 parent 73d2247 commit 9b8dbd5
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 140 deletions.
184 changes: 93 additions & 91 deletions .gas-snapshot

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions contracts.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"hardhat": {
"NORI": {
"proxyAddress": "0xEdf0bef28943aE9d2ee7d84cF1AEEC85588655b7"
"proxyAddress": "0xcb3a9Df3363315fb6E190A77C9421ecc6a92cfF0"
},
"BridgedPolygonNORI": {
"proxyAddress": "0xbF1134A069430ED15FC6AA4Ba7563ca340213eb6"
Expand Down Expand Up @@ -31,7 +31,7 @@
"proxyAddress": "0xb4c857d57b7130F033159958B5af4Fa3Bd0a04c6"
},
"NoriUSDC": {
"proxyAddress": "0x2AE26a38Bc11CE4FdfCaf0431D5a708b17E0dD99"
"proxyAddress": "0xEdf0bef28943aE9d2ee7d84cF1AEEC85588655b7"
}
},
"localhost": {
Expand Down
32 changes: 23 additions & 9 deletions contracts/Market.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {UInt256ArrayLib, AddressArrayLib} from "./ArrayLib.sol";
* and token balances that comprise the specific certificate for the amount purchased.
*
* The market maintains a "priority restricted threshold", which is a configurable threshold of supply that is
* always reserved to sell only to buyers who have the `ALLOWLIST_ROLE`. Purchases that would drop supply below
* always reserved to sell only to buyers who have the `PRIORITY_ALLOWLIST_ROLE`. Purchases that would drop supply below
* this threshold will revert without the correct role.
*
* ###### Additional behaviors and features
Expand All @@ -41,7 +41,8 @@ import {UInt256ArrayLib, AddressArrayLib} from "./ArrayLib.sol";
* - `MARKET_ADMIN_ROLE`: Can set the value of market configuration variables: fee percentage, fee wallet address,
* priority restricted threshold, purchasing token, and price multiple. Can execute replacement operations through
* the `replace` function. Can submit special orders through `swapWithoutFeeSpecialOrder`.
* - `ALLOWLIST_ROLE`: Can purchase from priority restricted supply.
* - `PRIORITY_ALLOWLIST_ROLE`: Can purchase from priority restricted supply.
* - `SWAP_ALLOWLIST_ROLE`: Can purchase using the `swap` endpoint.
* - [Can receive ERC1155 tokens](https://docs.openzeppelin.com/contracts/4.x/api/token/erc1155#IERC1155Receiver)
*
* ##### Inherits:
Expand Down Expand Up @@ -192,7 +193,14 @@ contract Market is
/**
* @notice Role conferring the ability to purchase supply when inventory is below the priority restricted threshold.
*/
bytes32 public constant ALLOWLIST_ROLE = keccak256("ALLOWLIST_ROLE");
bytes32 public constant PRIORITY_ALLOWLIST_ROLE =
keccak256("PRIORITY_ALLOWLIST_ROLE");

/**
* @notice Role conferring the ability to purchase using the `swap` endpoint.
*/
bytes32 public constant SWAP_ALLOWLIST_ROLE =
keccak256("SWAP_ALLOWLIST_ROLE");

/**
* @notice The number of decimal places reserved for Nori fee calculations.
Expand Down Expand Up @@ -373,7 +381,8 @@ contract Market is
_setPurchasingToken({purchasingToken: purchasingToken});
_setPriceMultiple({priceMultiple: priceMultiple_});
_grantRole({role: DEFAULT_ADMIN_ROLE, account: _msgSender()});
_grantRole({role: ALLOWLIST_ROLE, account: _msgSender()});
_grantRole({role: PRIORITY_ALLOWLIST_ROLE, account: _msgSender()});
_grantRole({role: SWAP_ALLOWLIST_ROLE, account: _msgSender()});
_grantRole({role: MARKET_ADMIN_ROLE, account: _msgSender()});
}

Expand Down Expand Up @@ -543,7 +552,7 @@ contract Market is

/**
* @notice Sets the current value of the priority restricted threshold, which is the amount of inventory
* that will always be reserved to sell only to buyers with the `ALLOWLIST_ROLE` role.
* that will always be reserved to sell only to buyers with the `PRIORITY_ALLOWLIST_ROLE` role.
* @dev Emits a `SetPriorityRestrictedThreshold` event.
*
* ##### Requirements:
Expand Down Expand Up @@ -674,6 +683,7 @@ contract Market is
* ##### Requirements:
*
* - Can only be used when this contract is not paused.
* - Can only be used if the message sender has the `SWAP_ALLOWLIST_ROLE`.
* @param recipient The address to which the certificate will be issued.
* @param amount The total amount of Removals being purchased.
* @param deadline The EIP2612 permit deadline in Unix time.
Expand All @@ -688,7 +698,7 @@ contract Market is
uint8 v,
bytes32 r,
bytes32 s
) external whenNotPaused {
) external whenNotPaused onlyRole(SWAP_ALLOWLIST_ROLE) {
_validateCertificateAmount({amount: amount});
SupplyAllocationData memory allocationData = _allocateRemovals({
certificateAmount: amount
Expand Down Expand Up @@ -728,10 +738,14 @@ contract Market is
*
* - Can only be used when this contract is not paused.
* - Can only be used if this contract has been granted approval to transfer the sender's ERC20 tokens.
* - Can only be used if the message sender has the `SWAP_ALLOWLIST_ROLE`.
* @param recipient The address to which the certificate will be issued.
* @param amount The total amount of Removals to purchase.
*/
function swap(address recipient, uint256 amount) external whenNotPaused {
function swap(
address recipient,
uint256 amount
) external whenNotPaused onlyRole(SWAP_ALLOWLIST_ROLE) {
_validateCertificateAmount({amount: amount});
SupplyAllocationData memory allocationData = _allocateRemovals({
certificateAmount: amount
Expand Down Expand Up @@ -847,7 +861,7 @@ contract Market is

/**
* @notice Returns the current value of the priority restricted threshold, which is the amount of inventory
* that will always be reserved to sell only to buyers with the `ALLOWLIST_ROLE` role.
* that will always be reserved to sell only to buyers with the `PRIORITY_ALLOWLIST_ROLE` role.
* @return The threshold of supply allowed for priority customers only.
*/
function getPriorityRestrictedThreshold() external view returns (uint256) {
Expand Down Expand Up @@ -1435,7 +1449,7 @@ contract Market is
b: certificateAmount
});
if (supplyAfterPurchase < _priorityRestrictedThreshold) {
if (!hasRole({role: ALLOWLIST_ROLE, account: _msgSender()})) {
if (!hasRole({role: PRIORITY_ALLOWLIST_ROLE, account: _msgSender()})) {
revert LowSupplyAllowlistRequired();
}
}
Expand Down
43 changes: 28 additions & 15 deletions docs/Market.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Each of these certificates is a non-transferrable, non-fungible token that owns
and token balances that comprise the specific certificate for the amount purchased.

The market maintains a "priority restricted threshold", which is a configurable threshold of supply that is
always reserved to sell only to buyers who have the `ALLOWLIST_ROLE`. Purchases that would drop supply below
always reserved to sell only to buyers who have the `PRIORITY_ALLOWLIST_ROLE`. Purchases that would drop supply below
this threshold will revert without the correct role.

###### Additional behaviors and features
Expand All @@ -23,7 +23,8 @@ state are pausable.
- `MARKET_ADMIN_ROLE`: Can set the value of market configuration variables: fee percentage, fee wallet address,
priority restricted threshold, purchasing token, and price multiple. Can execute replacement operations through
the `replace` function. Can submit special orders through `swapWithoutFeeSpecialOrder`.
- `ALLOWLIST_ROLE`: Can purchase from priority restricted supply.
- `PRIORITY_ALLOWLIST_ROLE`: Can purchase from priority restricted supply.
- `SWAP_ALLOWLIST_ROLE`: Can purchase using the `swap` endpoint.
- [Can receive ERC1155 tokens](https://docs.openzeppelin.com/contracts/4.x/api/token/erc1155#IERC1155Receiver)

##### Inherits:
Expand Down Expand Up @@ -137,17 +138,28 @@ restricted threshold.



### ALLOWLIST_ROLE
### PRIORITY_ALLOWLIST_ROLE

```solidity
bytes32 ALLOWLIST_ROLE
bytes32 PRIORITY_ALLOWLIST_ROLE
```

Role conferring the ability to purchase supply when inventory is below the priority restricted threshold.




### SWAP_ALLOWLIST_ROLE

```solidity
bytes32 SWAP_ALLOWLIST_ROLE
```

Role conferring the ability to purchase using the `swap` endpoint.




### FEE_DECIMALS

```solidity
Expand Down Expand Up @@ -485,7 +497,7 @@ function setPriorityRestrictedThreshold(uint256 threshold) external
```

Sets the current value of the priority restricted threshold, which is the amount of inventory
that will always be reserved to sell only to buyers with the `ALLOWLIST_ROLE` role.
that will always be reserved to sell only to buyers with the `PRIORITY_ALLOWLIST_ROLE` role.

<i>Emits a `SetPriorityRestrictedThreshold` event.

Expand Down Expand Up @@ -606,27 +618,27 @@ https://docs.openzeppelin.com/contracts/4.x/api/token/erc1155#ERC1155Receiver) f
### swap

```solidity
function swap(address recipient, address permitOwner, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external
function swap(address recipient, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external
```

Exchange ERC20 tokens for an ERC721 certificate by transferring ownership of the removals to the
certificate. Relies on the EIP-2612 permit extension to facilitate ERC20 token transfer.

<i>See [ERC20Permit](https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#ERC20Permit) for more.
The message sender must present a valid permit to this contract to temporarily authorize this market
to transfer the permit owner's ERC20 to complete the purchase. A certificate is minted in the Certificate contract
The message sender must sign and present a valid permit to this contract to temporarily authorize this market
to transfer their ERC20 to complete the purchase. A certificate is minted in the Certificate contract
to the specified recipient and the ERC20 is distributed to the suppliers of the carbon removals,
to the RestrictedNORI contract that controls any restricted tokens owed to the suppliers, and finally
to Nori Inc. as a market operator fee.

##### Requirements:

- Can only be used when this contract is not paused.</i>
- Can only be used when this contract is not paused.
- Can only be used if the message sender has the `SWAP_ALLOWLIST_ROLE`.</i>

| Name | Type | Description |
| ---- | ---- | ----------- |
| recipient | address | The address to which the certificate will be issued. |
| permitOwner | address | The address that signed the EIP2612 permit and will pay for the removals. |
| amount | uint256 | The total amount of Removals being purchased. |
| deadline | uint256 | The EIP2612 permit deadline in Unix time. |
| v | uint8 | The recovery identifier for the permit's secp256k1 signature. |
Expand Down Expand Up @@ -654,7 +666,8 @@ to Nori Inc. as a market operator fee.
##### Requirements:

- Can only be used when this contract is not paused.
- Can only be used if this contract has been granted approval to transfer the sender's ERC20 tokens.</i>
- Can only be used if this contract has been granted approval to transfer the sender's ERC20 tokens.
- Can only be used if the message sender has the `SWAP_ALLOWLIST_ROLE`.</i>

| Name | Type | Description |
| ---- | ---- | ----------- |
Expand Down Expand Up @@ -734,7 +747,7 @@ function getPriorityRestrictedThreshold() external view returns (uint256)
```

Returns the current value of the priority restricted threshold, which is the amount of inventory
that will always be reserved to sell only to buyers with the `ALLOWLIST_ROLE` role.
that will always be reserved to sell only to buyers with the `PRIORITY_ALLOWLIST_ROLE` role.



Expand Down Expand Up @@ -1180,9 +1193,9 @@ Validates the certificate purchase amount.
##### Requirements:

- Amount is not zero.
- Amount is divisible by 10^(18 - `_purchasingToken.decimals()` + FEE_DECIMALS). This requirement means that the
smallest purchase amount for a token with 18 decimals (e.g., NORI) and 2 FEE_DECIMALS is 100, whilst the smallest
purchase amount for a token with 6 decimals (e.g., USDC) and 2 FEE_DECIMALS is 100,000,000,000,000.</i>
- Amount is divisible by 10^(18 - `_purchasingToken.decimals()` + `FEE_DECIMALS`). This requirement means that the
smallest purchase amount for a token with 18 decimals (e.g., NORI) and 2 `FEE_DECIMALS` is 100, whilst the smallest
purchase amount for a token with 6 decimals (e.g., USDC) and 2 `FEE_DECIMALS` is 100,000,000,000,000.</i>

| Name | Type | Description |
| ---- | ---- | ----------- |
Expand Down
7 changes: 4 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Nori Smart Contracts

Nori's core product is the Nori _Removal_ Tonne (NRT). NRTs can be understood as carbon removal credits that are granted to a supplier for the CO2 they have removed from the atmosphere via [regenerative agriculture](https://www.weforum.org/agenda/2023/01/5-ways-to-scale-regenerative-agriculture-davos23/). For each tonne of carbon removed, a supplier is granted 1 NRT. Suppliers then consign their NRTs to Nori's marketplace, effectively listing them for sale at a fixed amount of USDC.
Nori's core product is the Nori _Removal_ Tonne (NRT). NRTs can be understood as carbon removal credits that are granted to a supplier for the CO2 they have removed from the atmosphere via [regenerative agriculture](https://www.weforum.org/agenda/2023/01/5-ways-to-scale-regenerative-agriculture-davos23/). For each tonne of carbon removed, a supplier is granted 1 NRT. Suppliers then consign their NRTs to Nori's marketplace, effectively listing them for sale at a fixed amount of USDC.

If a supplier is found to have released the sequestered carbon the corresponding Removals will be burned and funds from the insurance reserve used to replace them making the _Certificate_ and buyer whole. Automating the replacement of those burned Removals is on the future roadmap but is not implemented here.

Expand Down Expand Up @@ -46,7 +46,8 @@ The core swap market contract of the Nori platform. Removals are listed for sale

##### Swap mechanism

The `swap` function is the primary point of interaction with the market for buyers. Calls to the `swap` function include an amount of supported ERC20 tokens (_USDC_ or _BridgedPolygonNORI_) to spend and a recipient wallet address to which the _Certificate_ is minted. These calls also include a pre-signed authorization to transfer the corresponding amount of the supported ERC20 following the [ERC20Permit](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/draft-ERC20Permit.sol) pattern.
The `swap` function is the primary point of interaction with the market for buyers. Calls to the `swap` function include an amount of NRTs to purchase and a recipient wallet address to which the _Certificate_ is minted. These calls also include a pre-signed authorization to transfer the corresponding amount (including fees) of the supported ERC20 (_USDC_ or _BridgedPolygonNORI_) following the [ERC20Permit](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/draft-ERC20Permit.sol) pattern. Alternatively, the buyer can pre-approve the Market contract as a spender of the corresponding ERC20 and use the version
of `swap` that does not require permit arguments. Note that this previously public endpoint now requires buyers to have the `SWAP_ALLOWLIST_ROLE` to comply with sanctions laws and regulations.

The ERC20 tokens transferred from the buyer to this contract are distributed as follows:

Expand All @@ -60,7 +61,7 @@ An unsold _Removal_ can be withdrawn from the market (delisted for sale) by the

##### Priority Supply Mechanism

The market may be configured with a priority supply threshold. When supply listed for sale drops below this threshold purchases are restricted to addresses having the `ALLOWLIST_ROLE` role. This mechanism gives Nori the ability to reserve supply for pre-committed partnerships or other off-chain arrangements.
The market may be configured with a priority supply threshold. When supply listed for sale drops below this threshold purchases are restricted to addresses having the `PRIORITY_ALLOWLIST_ROLE` role. This mechanism gives Nori the ability to reserve supply for pre-committed partnerships or other off-chain arrangements.

### Support Libraries

Expand Down
27 changes: 27 additions & 0 deletions docs/UInt256ArrayLib.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,32 @@ new uint256[](100).fill(1).slice(0, 50); // returns: [:50]
| ---- | ---- | ----------- |
| sliced | uint256[] | The sliced array. |

### shrink

```solidity
function shrink(uint256[] values, uint256 length) internal pure returns (uint256[])
```

Shorten an array to specified length.

<i>Shortens the specified array to the specified length by directly overwriting
the length of the original array in storage.

##### Example usage:

```solidity
new uint256[](100).fill(1).shrink(50); // resizes the original array to length 50
```
-</i>

| Name | Type | Description |
| ---- | ---- | ----------- |
| values | uint256[] | The array to shorten. |
| length | uint256 | The desired length of the array. |

| Name | Type | Description |
| ---- | ---- | ----------- |
| [0] | uint256[] | values The shortened array. |



Loading

0 comments on commit 9b8dbd5

Please sign in to comment.