Skip to content

Commit 8071273

Browse files
author
telome
committed
Inherit ERC721 from OZ
1 parent e57ed7f commit 8071273

File tree

5 files changed

+44
-132
lines changed

5 files changed

+44
-132
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "lib/dss-test"]
22
path = lib/dss-test
33
url = https://github.com/makerdao/dss-test
4+
[submodule "lib/openzeppelin-contracts"]
5+
path = lib/openzeppelin-contracts
6+
url = https://github.com/OpenZeppelin/openzeppelin-contracts

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ Below is a non-exhaustive list of deviations from [laniakea-docs](https://github
1212
- The spec requires complete withdrawal only for the queue. This implementation supports partial withdrawals.
1313
- The spec calls the minting operation `claim`. This implementation uses `issue` to avoid confusion with the redeem-side concept of claiming funded amounts.
1414
- The spec tracks queue deposits through a shares-based accounting system. Since the deposited asset earns no yield while queued, shares would always be 1:1 with the underlying, so this implementation tracks deposits as direct balances instead.
15+
16+
## Notes
17+
18+
- 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.

lib/openzeppelin-contracts

Submodule openzeppelin-contracts added at fcbae53

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 Issue(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
require(identityNetwork == address(0) || IdentityNetworkLike(identityNetwork).isMember(owner), "NFATFacility/not-member");
267244

@@ -274,89 +251,15 @@ contract NFATFacility {
274251
emit Redeem(tokenId, amount);
275252
}
276253

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

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

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 issue
267268
vm.expectEmit(true, true, true, true);
268-
emit Issue(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 Issue(prime1, 0, 60 ether);
271272
uint256 tokenId = _issue(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.issue(prime1, 50 ether);
327328
}
328329

@@ -465,23 +466,23 @@ contract NFATFacilityTest is DssTest {
465466
_subscribe(prime1, 100 ether);
466467
uint256 tokenId = _issue(prime1, 100 ether);
467468

468-
vm.expectRevert("NFATFacility/not-authorized");
469+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InsufficientApproval.selector, prime2, tokenId));
469470
vm.prank(prime2); facility.transferFrom(prime1, prime2, tokenId);
470471
}
471472

472473
function testRevertTransferFromWrongFrom() public {
473474
_subscribe(prime1, 100 ether);
474475
uint256 tokenId = _issue(prime1, 100 ether);
475476

476-
vm.expectRevert("NFATFacility/wrong-from");
477+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721IncorrectOwner.selector, prime2, tokenId, prime1));
477478
vm.prank(prime1); facility.transferFrom(prime2, prime2, tokenId);
478479
}
479480

480481
function testRevertTransferFromZeroAddress() public {
481482
_subscribe(prime1, 100 ether);
482483
uint256 tokenId = _issue(prime1, 100 ether);
483484

484-
vm.expectRevert("NFATFacility/zero-address");
485+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, address(0)));
485486
vm.prank(prime1); facility.transferFrom(prime1, address(0), tokenId);
486487
}
487488

@@ -495,7 +496,7 @@ contract NFATFacilityTest is DssTest {
495496

496497
// Reverts when `to` is not a member
497498
idNet.setMember(prime2, false);
498-
vm.expectRevert("NFATFacility/to-not-member");
499+
vm.expectRevert("NFATFacility/not-member");
499500
vm.prank(prime1); facility.transferFrom(prime1, prime2, tokenId);
500501

501502
// Succeeds when `to` is a member
@@ -528,12 +529,12 @@ contract NFATFacilityTest is DssTest {
528529

529530
// Bad return value
530531
uint256 tokenId0 = _issue(prime1, 50 ether);
531-
vm.expectRevert("NFATFacility/unsafe-recipient");
532+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, address(badReceiver)));
532533
vm.prank(prime1); facility.safeTransferFrom(prime1, address(badReceiver), tokenId0);
533534

534535
// No onERC721Received at all
535536
uint256 tokenId1 = _issue(prime1, 50 ether);
536-
vm.expectRevert("NFATFacility/unsafe-recipient");
537+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidReceiver.selector, address(facility)));
537538
vm.prank(prime1); facility.safeTransferFrom(prime1, address(facility), tokenId1);
538539
}
539540

@@ -557,7 +558,7 @@ contract NFATFacilityTest is DssTest {
557558
_subscribe(prime1, 100 ether);
558559
uint256 tokenId = _issue(prime1, 100 ether);
559560

560-
vm.expectRevert("NFATFacility/not-authorized");
561+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidApprover.selector, prime2));
561562
vm.prank(prime2); facility.approve(prime2, tokenId);
562563
}
563564

@@ -569,23 +570,23 @@ contract NFATFacilityTest is DssTest {
569570
assertTrue(facility.isApprovedForAll(prime1, prime2));
570571
}
571572

572-
function testRevertSetApprovalForAllSelf() public {
573-
vm.expectRevert("NFATFacility/self-approval");
574-
vm.prank(prime1); facility.setApprovalForAll(prime1, true);
573+
function testRevertSetApprovalForAllZeroAddress() public {
574+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidOperator.selector, address(0)));
575+
vm.prank(prime1); facility.setApprovalForAll(address(0), true);
575576
}
576577

577578
function testRevertOwnerOfInvalidToken() public {
578-
vm.expectRevert("NFATFacility/invalid-token");
579+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, uint256(999)));
579580
facility.ownerOf(999);
580581
}
581582

582583
function testRevertBalanceOfZeroAddress() public {
583-
vm.expectRevert("NFATFacility/zero-address");
584+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721InvalidOwner.selector, address(0)));
584585
facility.balanceOf(address(0));
585586
}
586587

587588
function testRevertGetApprovedInvalidToken() public {
588-
vm.expectRevert("NFATFacility/invalid-token");
589+
vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, uint256(999)));
589590
facility.getApproved(999);
590591
}
591592

0 commit comments

Comments
 (0)