Skip to content

Commit d9dde90

Browse files
author
telome
committed
Inherit ERC721 from OZ
1 parent e374517 commit d9dde90

File tree

3 files changed

+40
-132
lines changed

3 files changed

+40
-132
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ Below is a non-exhaustive list of deviations from [laniakea-docs](https://github
1010
- The spec requires `{principal, depositor, mintedAt}` to be stored on-chain for each NFAT. These fields are not used by any contract logic and are therefore omitted from storage.
1111
- The spec describes burning the NFAT on full redemption. This does not fit well with deals other than single-payment-at-maturity (e.g., loans with periodic interest), as the contract has no knowledge of deal terms and burning would have to be coordinated off-chain with no on-chain purpose. Instead, NFATs are never burned and residual payments are tracked off-chain.
1212
- The spec requires complete withdrawal only for the queue. This implementation supports partial withdrawals.
13+
14+
## Notes
15+
16+
- The contract inherits OpenZeppelin's ERC721 which advertises support for the ERC721Metadata extension via `supportsInterface`. However, no base URI is configured, so `tokenURI()` returns an empty string for all tokens.

src/NFATFacility.sol

Lines changed: 19 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
pragma solidity ^0.8.24;
1818

19+
import { ERC721 } from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
20+
1921
interface GemLike {
2022
function transferFrom(address from, address to, uint256 amount) external;
2123
function transfer(address to, uint256 amount) external;
@@ -25,19 +27,10 @@ interface IdentityNetworkLike {
2527
function isMember(address account) external view returns (bool);
2628
}
2729

28-
interface ERC721ReceiverLike {
29-
function onERC721Received(
30-
address operator,
31-
address from,
32-
uint256 tokenId,
33-
bytes calldata data
34-
) external returns (bytes4);
35-
}
36-
3730
/// @title NFATFacility
3831
/// @notice Non-Fungible Allocation Token Facility for bespoke capital deployment deals
3932
/// @dev Implements queue-based deposits and ERC-721 NFAT minting
40-
contract NFATFacility {
33+
contract NFATFacility is ERC721 {
4134

4235
// --- Immutables ---
4336

@@ -62,13 +55,7 @@ contract NFATFacility {
6255

6356
// --- NFAT Storage ---
6457

65-
string public name;
66-
string public symbol;
6758
uint256 public nextTokenId;
68-
mapping(uint256 tokenId => address owner) internal _owners;
69-
mapping(address owner => uint256 count) internal _balances;
70-
mapping(uint256 tokenId => address approved) internal _tokenApprovals;
71-
mapping(address owner => mapping(address operator => bool approved)) internal _operatorApprovals;
7259

7360
// --- Events: Access Control ---
7461

@@ -93,12 +80,6 @@ contract NFATFacility {
9380
event Fund(uint256 indexed tokenId, address indexed funder, uint256 amount);
9481
event Redeem(uint256 indexed tokenId, uint256 amount);
9582

96-
// --- Events: ERC-721 ---
97-
98-
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
99-
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
100-
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
101-
10283
// --- Modifiers ---
10384

10485
modifier auth() {
@@ -123,11 +104,11 @@ contract NFATFacility {
123104

124105
// --- Constructor ---
125106

126-
constructor(address gem_, address almProxy_, string memory name_, string memory symbol_) {
107+
constructor(address gem_, address almProxy_, string memory name_, string memory symbol_)
108+
ERC721(name_, symbol_)
109+
{
127110
gem = GemLike(gem_);
128111
almProxy = almProxy_;
129-
name = name_;
130-
symbol = symbol_;
131112
wards[msg.sender] = 1;
132113
emit Rely(msg.sender);
133114
}
@@ -218,22 +199,18 @@ contract NFATFacility {
218199
require(amount > 0, "NFATFacility/zero-amount");
219200
require(deposits[target] >= amount, "NFATFacility/insufficient-deposits");
220201

221-
require(identityNetwork == address(0) || IdentityNetworkLike(identityNetwork).isMember(target), "NFATFacility/target-not-member");
222-
223202
uint256 tokenId = nextTokenId++;
224203

225204
// Effects - Queue
226205
unchecked { deposits[target] -= amount; }
227206

228-
// Effects - NFAT
229-
_owners[tokenId] = target;
230-
_balances[target] += 1;
207+
// Effects - NFAT (identity network check in _update)
208+
_mint(target, tokenId);
231209

232210
// Interactions
233211
gem.transfer(almProxy, amount);
234212

235213
emit Claim(target, tokenId, amount);
236-
emit Transfer(address(0), target, tokenId);
237214
}
238215

239216
// --- Redeem Functions ---
@@ -242,7 +219,7 @@ contract NFATFacility {
242219
/// @param tokenId The NFAT to fund
243220
/// @param amount The amount of gem to deposit
244221
function fund(uint256 tokenId, uint256 amount) external {
245-
require(_owners[tokenId] != address(0), "NFATFacility/invalid-token");
222+
require(_ownerOf(tokenId) != address(0), "NFATFacility/invalid-token");
246223
require(amount > 0, "NFATFacility/zero-amount");
247224

248225
// Effects
@@ -261,7 +238,7 @@ contract NFATFacility {
261238
require(amount > 0, "NFATFacility/zero-amount");
262239
require(funded[tokenId] >= amount, "NFATFacility/insufficient-funded");
263240

264-
address owner = _owners[tokenId];
241+
address owner = _ownerOf(tokenId);
265242
require(msg.sender == owner, "NFATFacility/not-owner");
266243

267244
// Effects
@@ -273,89 +250,15 @@ contract NFATFacility {
273250
emit Redeem(tokenId, amount);
274251
}
275252

276-
// --- ERC-721 Functions ---
277-
278-
function ownerOf(uint256 tokenId) public view returns (address) {
279-
address owner = _owners[tokenId];
280-
require(owner != address(0), "NFATFacility/invalid-token");
281-
return owner;
282-
}
283-
284-
function balanceOf(address owner) external view returns (uint256) {
285-
require(owner != address(0), "NFATFacility/zero-address");
286-
return _balances[owner];
287-
}
288-
289-
function approve(address to, uint256 tokenId) external {
290-
address owner = ownerOf(tokenId);
291-
require(msg.sender == owner || _operatorApprovals[owner][msg.sender], "NFATFacility/not-authorized");
292-
_tokenApprovals[tokenId] = to;
293-
emit Approval(owner, to, tokenId);
294-
}
295-
296-
function getApproved(uint256 tokenId) external view returns (address) {
297-
require(_owners[tokenId] != address(0), "NFATFacility/invalid-token");
298-
return _tokenApprovals[tokenId];
299-
}
300-
301-
function setApprovalForAll(address operator, bool approved) external {
302-
require(operator != msg.sender, "NFATFacility/self-approval");
303-
_operatorApprovals[msg.sender][operator] = approved;
304-
emit ApprovalForAll(msg.sender, operator, approved);
305-
}
306-
307-
function isApprovedForAll(address owner, address operator) external view returns (bool) {
308-
return _operatorApprovals[owner][operator];
309-
}
310-
311-
function transferFrom(address from, address to, uint256 tokenId) public {
312-
require(_isApprovedOrOwner(msg.sender, tokenId), "NFATFacility/not-authorized");
313-
require(ownerOf(tokenId) == from, "NFATFacility/wrong-from");
314-
require(to != address(0), "NFATFacility/zero-address");
315-
316-
require(identityNetwork == address(0) || IdentityNetworkLike(identityNetwork).isMember(to), "NFATFacility/to-not-member");
317-
318-
// Clear approval
319-
_tokenApprovals[tokenId] = address(0);
320-
321-
// Effects
322-
_balances[from] -= 1;
323-
_balances[to] += 1;
324-
_owners[tokenId] = to;
325-
326-
emit Transfer(from, to, tokenId);
327-
}
328-
329-
function safeTransferFrom(address from, address to, uint256 tokenId) external {
330-
safeTransferFrom(from, to, tokenId, "");
331-
}
332-
333-
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {
334-
transferFrom(from, to, tokenId);
335-
require(_checkOnERC721Received(from, to, tokenId, data), "NFATFacility/unsafe-recipient");
336-
}
337-
338-
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
339-
address owner = ownerOf(tokenId);
340-
return (spender == owner || _tokenApprovals[tokenId] == spender || _operatorApprovals[owner][spender]);
341-
}
342-
343-
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) internal returns (bool) {
344-
if (to.code.length == 0) {
345-
return true;
346-
}
347-
try ERC721ReceiverLike(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
348-
return retval == ERC721ReceiverLike.onERC721Received.selector;
349-
} catch {
350-
return false;
351-
}
352-
}
353-
354-
// --- ERC-165 ---
253+
// --- ERC-721 Overrides ---
355254

356-
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
357-
return
358-
interfaceId == 0x01ffc9a7 || // ERC-165
359-
interfaceId == 0x80ac58cd; // ERC-721
255+
/// @dev OZ's _mint and transferFrom both revert before calling _update when to == address(0),
256+
/// and _burn is never invoked, so `to` is guaranteed to be non-zero here.
257+
function _update(address to, uint256 tokenId, address auth_) internal override returns (address) {
258+
require(
259+
identityNetwork == address(0) || IdentityNetworkLike(identityNetwork).isMember(to),
260+
"NFATFacility/not-member"
261+
);
262+
return super._update(to, tokenId, auth_);
360263
}
361264
}

test/NFATFacility.t.sol

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "dss-test/DssTest.sol";
55
import { NFATFacility } from "src/NFATFacility.sol";
66
import { NFATDeploy } from "deploy/NFATDeploy.sol";
77
import { NFATInit, NFATConfig } from "deploy/NFATInit.sol";
8+
import { IERC721Errors } from "openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol";
89

910
interface SUsdsLike {
1011
function balanceOf(address) external view returns (uint256);
@@ -265,9 +266,9 @@ contract NFATFacilityTest is DssTest {
265266

266267
// First claim
267268
vm.expectEmit(true, true, true, true);
268-
emit Claim(prime1, 0, 60 ether);
269-
vm.expectEmit(true, true, true, true);
270269
emit Transfer(address(0), prime1, 0);
270+
vm.expectEmit(true, true, true, true);
271+
emit Claim(prime1, 0, 60 ether);
271272
uint256 tokenId = _claim(prime1, 60 ether);
272273

273274
assertEq(tokenId, 0);
@@ -322,7 +323,7 @@ contract NFATFacilityTest is DssTest {
322323

323324
_subscribe(prime1, 100 ether);
324325

325-
vm.expectRevert("NFATFacility/target-not-member");
326+
vm.expectRevert("NFATFacility/not-member");
326327
vm.prank(operator); facility.claim(prime1, 50 ether);
327328
}
328329

@@ -450,23 +451,23 @@ contract NFATFacilityTest is DssTest {
450451
_subscribe(prime1, 100 ether);
451452
uint256 tokenId = _claim(prime1, 100 ether);
452453

453-
vm.expectRevert("NFATFacility/not-authorized");
454+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, prime2, tokenId));
454455
vm.prank(prime2); facility.transferFrom(prime1, prime2, tokenId);
455456
}
456457

457458
function testRevertTransferFromWrongFrom() public {
458459
_subscribe(prime1, 100 ether);
459460
uint256 tokenId = _claim(prime1, 100 ether);
460461

461-
vm.expectRevert("NFATFacility/wrong-from");
462+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721IncorrectOwner.selector, prime2, tokenId, prime1));
462463
vm.prank(prime1); facility.transferFrom(prime2, prime2, tokenId);
463464
}
464465

465466
function testRevertTransferFromZeroAddress() public {
466467
_subscribe(prime1, 100 ether);
467468
uint256 tokenId = _claim(prime1, 100 ether);
468469

469-
vm.expectRevert("NFATFacility/zero-address");
470+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, address(0)));
470471
vm.prank(prime1); facility.transferFrom(prime1, address(0), tokenId);
471472
}
472473

@@ -480,7 +481,7 @@ contract NFATFacilityTest is DssTest {
480481

481482
// Reverts when `to` is not a member
482483
idNet.setMember(prime2, false);
483-
vm.expectRevert("NFATFacility/to-not-member");
484+
vm.expectRevert("NFATFacility/not-member");
484485
vm.prank(prime1); facility.transferFrom(prime1, prime2, tokenId);
485486

486487
// Succeeds when `to` is a member
@@ -513,12 +514,12 @@ contract NFATFacilityTest is DssTest {
513514

514515
// Bad return value
515516
uint256 tokenId0 = _claim(prime1, 50 ether);
516-
vm.expectRevert("NFATFacility/unsafe-recipient");
517+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, address(badReceiver)));
517518
vm.prank(prime1); facility.safeTransferFrom(prime1, address(badReceiver), tokenId0);
518519

519520
// No onERC721Received at all
520521
uint256 tokenId1 = _claim(prime1, 50 ether);
521-
vm.expectRevert("NFATFacility/unsafe-recipient");
522+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, address(facility)));
522523
vm.prank(prime1); facility.safeTransferFrom(prime1, address(facility), tokenId1);
523524
}
524525

@@ -542,7 +543,7 @@ contract NFATFacilityTest is DssTest {
542543
_subscribe(prime1, 100 ether);
543544
uint256 tokenId = _claim(prime1, 100 ether);
544545

545-
vm.expectRevert("NFATFacility/not-authorized");
546+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidApprover.selector, prime2));
546547
vm.prank(prime2); facility.approve(prime2, tokenId);
547548
}
548549

@@ -554,23 +555,23 @@ contract NFATFacilityTest is DssTest {
554555
assertTrue(facility.isApprovedForAll(prime1, prime2));
555556
}
556557

557-
function testRevertSetApprovalForAllSelf() public {
558-
vm.expectRevert("NFATFacility/self-approval");
559-
vm.prank(prime1); facility.setApprovalForAll(prime1, true);
558+
function testRevertSetApprovalForAllZeroAddress() public {
559+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidOperator.selector, address(0)));
560+
vm.prank(prime1); facility.setApprovalForAll(address(0), true);
560561
}
561562

562563
function testRevertOwnerOfInvalidToken() public {
563-
vm.expectRevert("NFATFacility/invalid-token");
564+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, uint256(999)));
564565
facility.ownerOf(999);
565566
}
566567

567568
function testRevertBalanceOfZeroAddress() public {
568-
vm.expectRevert("NFATFacility/zero-address");
569+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidOwner.selector, address(0)));
569570
facility.balanceOf(address(0));
570571
}
571572

572573
function testRevertGetApprovedInvalidToken() public {
573-
vm.expectRevert("NFATFacility/invalid-token");
574+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, uint256(999)));
574575
facility.getApproved(999);
575576
}
576577

0 commit comments

Comments
 (0)