Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
39 changes: 17 additions & 22 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
name: CI
name: test

permissions: {}

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci
on: [push, pull_request]

jobs:
check:
strategy:
fail-fast: true

name: Foundry project
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v3
with:
persist-credentials: false
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Show Forge version
run: forge --version

- name: Run Forge fmt
run: forge fmt --check
with:
version: stable

- name: Run Forge build
run: forge build --sizes
run: |
forge --version
forge build --sizes
id: build

- name: Run Forge tests
run: forge test -vvv
run: |
forge test -vvv
id: test
env:
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
8 changes: 7 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.24"
optimizer = true
optimizer_runs = 200
evmVersion = "cancun"
verbosity = 1

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
[lint]
lint_on_build = false
147 changes: 36 additions & 111 deletions src/NFATFacility.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}

interface WhitelistLike {
function canTransfer(address from, address to) external view returns (bool);
}

/// @title NFATFacility
/// @notice Non-Fungible Allocation Token Facility for bespoke capital deployment deals
/// @dev Implements queue-based deposits, ERC-721 NFAT minting, and redemption mechanics
/// @dev Implements queue-based deposits and ERC-721 NFAT minting
contract NFATFacility {

// --- Immutables ---
Expand All @@ -40,17 +44,16 @@ contract NFATFacility {

// --- Queue Storage ---

uint256 public totalDeposits;
mapping(address depositor => uint256 amount) public deposits;

// --- NFAT Storage ---

uint256 public nextTokenId;

struct NFATData {
uint256 principal; // Current principal (mutable via spend)
address depositor; // Original Prime address (immutable)
uint40 mintedAt; // Mint timestamp
uint256 principal;
address depositor;
uint40 mintedAt;
}

mapping(uint256 tokenId => NFATData data) internal _nfats;
Expand All @@ -59,14 +62,9 @@ contract NFATFacility {
mapping(uint256 tokenId => address approved) internal _tokenApprovals;
mapping(address owner => mapping(address operator => bool approved)) internal _operatorApprovals;

// --- Redeemer Storage ---

address public redeemer;

// --- Whitelist Storage ---

bool public whitelistEnabled;
mapping(address account => bool allowed) public whitelist;
address public whitelist;

// --- Events: Access Control ---

Expand All @@ -89,14 +87,9 @@ contract NFATFacility {
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

// --- Events: Redeemer ---

event SetRedeemer(address indexed redeemer);
// --- Events: File ---

// --- Events: Whitelist ---

event WhitelistEnabled(bool enabled);
event WhitelistUpdated(address indexed account, bool allowed);
event File(bytes32 indexed what, address data);

// --- Modifiers ---

Expand Down Expand Up @@ -128,7 +121,7 @@ contract NFATFacility {
emit Rely(msg.sender);
}

// --- Access Control Functions ---
// --- Admin Functions ---

function rely(address usr) external auth {
wards[usr] = 1;
Expand Down Expand Up @@ -160,7 +153,7 @@ contract NFATFacility {
emit SetRoleAction(role, sig, enabled);
}

function stop() external auth {
function stop() external roleAuth {
stopped = true;
emit Stop();
}
Expand All @@ -170,34 +163,39 @@ contract NFATFacility {
emit Start();
}

function file(bytes32 what, address data) external auth {
if (what == "whitelist") whitelist = data;
else revert("NFATFacility/file-unrecognized-param");
emit File(what, data);
}

// --- Queue Functions ---

/// @notice Prime deposits sUSDS into the queue
/// @param amount The amount of sUSDS to deposit
function subscribe(uint256 amount) external notStopped {
function subscribe(uint256 amount) external {
require(amount > 0, "NFATFacility/zero-amount");

// Effects
deposits[msg.sender] += amount;
totalDeposits += amount;

// Interactions
require(sUSDS.transferFrom(msg.sender, address(this), amount), "NFATFacility/transfer-failed");
sUSDS.transferFrom(msg.sender, address(this), amount);

emit Subscribe(msg.sender, amount);
}

/// @notice Prime withdraws all deposited sUSDS (full exit from queue)
function withdraw() external notStopped {
uint256 amount = deposits[msg.sender];
require(amount > 0, "NFATFacility/no-deposits");
/// @notice Prime withdraws sUSDS from the queue
/// @param amount The amount of sUSDS to withdraw
function withdraw(uint256 amount) external {
require(amount > 0, "NFATFacility/zero-amount");
require(deposits[msg.sender] >= amount, "NFATFacility/insufficient-deposits");

// Effects
deposits[msg.sender] = 0;
totalDeposits -= amount;
deposits[msg.sender] -= amount;

// Interactions
require(sUSDS.transfer(msg.sender, amount), "NFATFacility/transfer-failed");
sUSDS.transfer(msg.sender, amount);

emit Withdraw(msg.sender, amount);
}
Expand All @@ -209,11 +207,12 @@ contract NFATFacility {
require(amount > 0, "NFATFacility/zero-amount");
require(deposits[target] >= amount, "NFATFacility/insufficient-deposits");

require(whitelist == address(0) || WhitelistLike(whitelist).canTransfer(address(0), target), "NFATFacility/transfer-denied");

uint256 tokenId = nextTokenId++;

// Effects - Queue
deposits[target] -= amount;
totalDeposits -= amount;

// Effects - NFAT
_nfats[tokenId] = NFATData({
Expand All @@ -225,7 +224,7 @@ contract NFATFacility {
_balances[target] += 1;

// Interactions
require(sUSDS.transfer(almProxy, amount), "NFATFacility/transfer-failed");
sUSDS.transfer(almProxy, amount);

emit Claim(target, tokenId, amount);
emit Transfer(address(0), target, tokenId);
Expand Down Expand Up @@ -266,14 +265,12 @@ contract NFATFacility {
return _operatorApprovals[owner][operator];
}

function transferFrom(address from, address to, uint256 tokenId) public notStopped {
function transferFrom(address from, address to, uint256 tokenId) public {
require(_isApprovedOrOwner(msg.sender, tokenId), "NFATFacility/not-authorized");
require(ownerOf(tokenId) == from, "NFATFacility/wrong-from");
require(to != address(0), "NFATFacility/zero-address");

if (whitelistEnabled) {
require(whitelist[to], "NFATFacility/not-whitelisted");
}
require(whitelist == address(0) || WhitelistLike(whitelist).canTransfer(from, to), "NFATFacility/transfer-denied");

// Clear approval
_tokenApprovals[tokenId] = address(0);
Expand Down Expand Up @@ -311,79 +308,8 @@ contract NFATFacility {
}
}

// --- Redeemer Functions ---

/// @notice Set the redeemer contract address
/// @param redeemer_ The address of the redeemer contract
function setRedeemer(address redeemer_) external auth {
redeemer = redeemer_;
emit SetRedeemer(redeemer_);
}

/// @notice Burns an NFAT token (only callable by redeemer)
/// @param tokenId The NFAT to burn
function burn(uint256 tokenId) external {
require(msg.sender == redeemer, "NFATFacility/not-redeemer");

address owner = _owners[tokenId];
require(owner != address(0), "NFATFacility/invalid-token");

// Effects - Burn NFAT
_tokenApprovals[tokenId] = address(0);
_balances[owner] -= 1;
delete _owners[tokenId];
delete _nfats[tokenId];

emit Transfer(owner, address(0), tokenId);
}

/// @notice Reduces principal of an NFAT (only callable by redeemer)
/// @param tokenId The NFAT to modify
/// @param amount The amount to reduce principal by
function reducePrincipal(uint256 tokenId, uint256 amount) external {
require(msg.sender == redeemer, "NFATFacility/not-redeemer");
require(_owners[tokenId] != address(0), "NFATFacility/invalid-token");

NFATData storage nfat = _nfats[tokenId];
require(nfat.principal >= amount, "NFATFacility/exceeds-principal");

nfat.principal -= amount;
}

/// @notice Check if an address is approved or owner of a token
/// @param spender The address to check
/// @param tokenId The token to check
/// @return Whether the spender is approved or owner
function isApprovedOrOwner(address spender, uint256 tokenId) external view returns (bool) {
return _isApprovedOrOwner(spender, tokenId);
}

// --- Whitelist Functions ---

/// @notice Enable or disable transfer restrictions
/// @param enabled Whether to enable the whitelist
function setWhitelistEnabled(bool enabled) external roleAuth {
whitelistEnabled = enabled;
emit WhitelistEnabled(enabled);
}

/// @notice Add or remove an address from the whitelist
/// @param account The address to update
/// @param allowed Whether the address is allowed
function setWhitelist(address account, bool allowed) external roleAuth {
whitelist[account] = allowed;
emit WhitelistUpdated(account, allowed);
}

// --- View Functions ---

/// @notice Get a depositor's balance in the queue
/// @param depositor The address to query
/// @return The deposited sUSDS amount
function getQueueBalance(address depositor) external view returns (uint256) {
return deposits[depositor];
}

/// @notice Check if a user has a specific role
/// @param usr The address to check
/// @param role The role ID
Expand All @@ -402,7 +328,7 @@ contract NFATFacility {

/// @notice Get the principal of an NFAT
/// @param tokenId The NFAT to query
/// @return The current principal
/// @return The principal
function getPrincipal(uint256 tokenId) external view returns (uint256) {
require(_owners[tokenId] != address(0), "NFATFacility/invalid-token");
return _nfats[tokenId].principal;
Expand All @@ -424,7 +350,7 @@ contract NFATFacility {
return _nfats[tokenId].mintedAt;
}

// --- ERC-721 Metadata (Optional) ---
// --- ERC-721 Metadata ---

function name() external pure returns (string memory) {
return "Non-Fungible Allocation Token";
Expand All @@ -439,8 +365,7 @@ contract NFATFacility {
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC-165
interfaceId == 0x80ac58cd || // ERC-721
interfaceId == 0x5b5e139f; // ERC-721 Metadata
interfaceId == 0x80ac58cd; // ERC-721
}
}

Expand Down
Loading