Skip to content

Commit 85a6df1

Browse files
authored
Merge pull request #23 from ProjectOpenSea/ryan/add-transfer-validator
add transfer validator contracts
2 parents 6077378 + fee74c5 commit 85a6df1

10 files changed

+440
-2
lines changed

.git-blame-ignore-revs

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
# Github Actions automatically updated formatting with forge fmt\n2d96311b29055c5b9a0b632176ce3b8d78a23a89
44
# Github Actions automatically updated formatting with forge fmt\ne2d336bbb0331c7716c16fed64f51be6c270ad02
55
# Github Actions automatically updated formatting with forge fmt\n3458423c8716bdcd3875cde4597dcae377a16620
6+
# Github Actions automatically updated formatting with forge fmt\n2ee13e093c7f5aee04944b79dadcf16089cbb560
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
interface ICreatorToken {
5+
event TransferValidatorUpdated(address oldValidator, address newValidator);
6+
7+
function getTransferValidator() external view returns (address validator);
8+
9+
function getTransferValidationFunction() external view returns (bytes4 functionSignature, bool isViewFunction);
10+
11+
function setTransferValidator(address validator) external;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
interface ITransferValidator721 {
5+
/// @notice Ensure that a transfer has been authorized for a specific tokenId
6+
function validateTransfer(address caller, address from, address to, uint256 tokenId) external view;
7+
}
8+
9+
interface ITransferValidator1155 {
10+
/// @notice Ensure that a transfer has been authorized for a specific amount of a specific tokenId, and reduce the transferable amount remaining
11+
function validateTransfer(address caller, address from, address to, uint256 tokenId, uint256 amount) external;
12+
}

src/reference/ExampleNFT.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ contract ExampleNFT is AbstractNFT {
7171
svg.prop("text-anchor", "middle"),
7272
svg.prop("font-size", "48"),
7373
svg.prop("fill", "black")
74-
),
74+
),
7575
children: LibString.toString(tokenId)
7676
})
77-
)
77+
)
7878
});
7979
}
8080

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
import {Ownable} from "solady/src/auth/Ownable.sol";
5+
import {ERC1155} from "solady/src/tokens/ERC1155.sol";
6+
import {ERC1155ConduitPreapproved_Solady} from "../tokens/erc1155/ERC1155ConduitPreapproved_Solady.sol";
7+
import {TokenTransferValidator, TokenTransferValidatorStorage} from "./lib/TokenTransferValidator.sol";
8+
import {ICreatorToken} from "../interfaces/transfer-validated/ICreatorToken.sol";
9+
import {ITransferValidator1155} from "../interfaces/transfer-validated/ITransferValidator.sol";
10+
11+
contract ERC1155ShipyardTransferValidated is ERC1155ConduitPreapproved_Solady, TokenTransferValidator, Ownable {
12+
using TokenTransferValidatorStorage for TokenTransferValidatorStorage.Layout;
13+
14+
constructor(address initialTransferValidator) ERC1155ConduitPreapproved_Solady() {
15+
// Set the initial contract owner.
16+
_initializeOwner(msg.sender);
17+
18+
// Set the initial transfer validator.
19+
if (initialTransferValidator != address(0)) {
20+
_setTransferValidator(initialTransferValidator);
21+
}
22+
}
23+
24+
/// @notice Returns the transfer validation function used.
25+
function getTransferValidationFunction() external pure returns (bytes4 functionSignature, bool isViewFunction) {
26+
functionSignature = ITransferValidator1155.validateTransfer.selector;
27+
isViewFunction = true;
28+
}
29+
30+
/// @notice Set the transfer validator. Only callable by the token owner.
31+
function setTransferValidator(address newValidator) external onlyOwner {
32+
// Set the new transfer validator.
33+
_setTransferValidator(newValidator);
34+
}
35+
36+
/// @dev Override this function to return true if `_beforeTokenTransfer` is used.
37+
function _useBeforeTokenTransfer() internal view virtual override returns (bool) {
38+
return true;
39+
}
40+
41+
/// @dev Hook that is called before any token transfer. This includes minting and burning.
42+
function _beforeTokenTransfer(
43+
address from,
44+
address to,
45+
uint256[] memory ids,
46+
uint256[] memory amounts,
47+
bytes memory /* data */
48+
) internal virtual override {
49+
if (from != address(0) && to != address(0)) {
50+
// Call the transfer validator if one is set.
51+
address transferValidator = TokenTransferValidatorStorage.layout()._transferValidator;
52+
if (transferValidator != address(0)) {
53+
for (uint256 i = 0; i < ids.length; i++) {
54+
ITransferValidator1155(transferValidator).validateTransfer(msg.sender, from, to, ids[i], amounts[i]);
55+
}
56+
}
57+
}
58+
}
59+
60+
/// @dev Override supportsInterface to additionally return true for ICreatorToken.
61+
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155) returns (bool) {
62+
return interfaceId == type(ICreatorToken).interfaceId || ERC1155.supportsInterface(interfaceId);
63+
}
64+
65+
/// @dev Replace me with the token name.
66+
function name() public view virtual returns (string memory) {
67+
return "ERC1155ShipyardTransferValidated";
68+
}
69+
70+
/// @dev Replace me with the token symbol.
71+
function symbol() public view virtual returns (string memory) {
72+
return "ERC1155-S-TV";
73+
}
74+
75+
/// @dev Replace me with the token URI.
76+
function uri(uint256 /* id */ ) public view virtual override returns (string memory) {
77+
return "";
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
import {Ownable} from "solady/src/auth/Ownable.sol";
5+
import {ERC721} from "solady/src/tokens/ERC721.sol";
6+
import {ERC721ConduitPreapproved_Solady} from "../tokens/erc721/ERC721ConduitPreapproved_Solady.sol";
7+
import {TokenTransferValidator, TokenTransferValidatorStorage} from "./lib/TokenTransferValidator.sol";
8+
import {ICreatorToken} from "../interfaces/transfer-validated/ICreatorToken.sol";
9+
import {ITransferValidator721} from "../interfaces/transfer-validated/ITransferValidator.sol";
10+
11+
contract ERC721ShipyardTransferValidated is ERC721ConduitPreapproved_Solady, TokenTransferValidator, Ownable {
12+
using TokenTransferValidatorStorage for TokenTransferValidatorStorage.Layout;
13+
14+
constructor(address initialTransferValidator) ERC721ConduitPreapproved_Solady() {
15+
// Set the initial contract owner.
16+
_initializeOwner(msg.sender);
17+
18+
// Set the initial transfer validator.
19+
if (initialTransferValidator != address(0)) {
20+
_setTransferValidator(initialTransferValidator);
21+
}
22+
}
23+
24+
/// @notice Returns the transfer validation function used.
25+
function getTransferValidationFunction() external pure returns (bytes4 functionSignature, bool isViewFunction) {
26+
functionSignature = ITransferValidator721.validateTransfer.selector;
27+
isViewFunction = false;
28+
}
29+
30+
/// @notice Set the transfer validator. Only callable by the token owner.
31+
function setTransferValidator(address newValidator) external onlyOwner {
32+
// Set the new transfer validator.
33+
_setTransferValidator(newValidator);
34+
}
35+
36+
/// @dev Hook that is called before any token transfer. This includes minting and burning.
37+
function _beforeTokenTransfer(address from, address to, uint256 id) internal virtual override {
38+
if (from != address(0) && to != address(0)) {
39+
// Call the transfer validator if one is set.
40+
address transferValidator = TokenTransferValidatorStorage.layout()._transferValidator;
41+
if (transferValidator != address(0)) {
42+
ITransferValidator721(transferValidator).validateTransfer(msg.sender, from, to, id);
43+
}
44+
}
45+
}
46+
47+
/// @dev Override supportsInterface to additionally return true for ICreatorToken.
48+
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) {
49+
return interfaceId == type(ICreatorToken).interfaceId || ERC721.supportsInterface(interfaceId);
50+
}
51+
52+
/// @dev Replace me with the token name.
53+
function name() public view virtual override returns (string memory) {
54+
return "ERC721ShipyardTransferValidated";
55+
}
56+
57+
/// @dev Replace me with the token symbol.
58+
function symbol() public view virtual override returns (string memory) {
59+
return "ERC721-S-TV";
60+
}
61+
62+
/// @dev Replace me with the token URI.
63+
function tokenURI(uint256 /* id */ ) public view virtual override returns (string memory) {
64+
return "";
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
import {ICreatorToken} from "../../interfaces/transfer-validated/ICreatorToken.sol";
5+
6+
library TokenTransferValidatorStorage {
7+
struct Layout {
8+
/// @dev Store the transfer validator. The null address means no transfer validator is set.
9+
address _transferValidator;
10+
}
11+
12+
bytes32 internal constant STORAGE_SLOT = keccak256("contracts.storage.tokenTransferValidator");
13+
14+
function layout() internal pure returns (Layout storage l) {
15+
bytes32 slot = STORAGE_SLOT;
16+
assembly {
17+
l.slot := slot
18+
}
19+
}
20+
}
21+
22+
/**
23+
* @title TokenTransferValidator
24+
* @notice Functionality to use a transfer validator.
25+
*/
26+
abstract contract TokenTransferValidator is ICreatorToken {
27+
using TokenTransferValidatorStorage for TokenTransferValidatorStorage.Layout;
28+
29+
/// @notice Revert with an error if the transfer validator is being set to the same address.
30+
error SameTransferValidator();
31+
32+
/// @notice Returns the currently active transfer validator.
33+
/// The null address means no transfer validator is set.
34+
function getTransferValidator() external view returns (address) {
35+
return TokenTransferValidatorStorage.layout()._transferValidator;
36+
}
37+
38+
/// @notice Set the transfer validator.
39+
/// The external method that uses this must include access control.
40+
function _setTransferValidator(address newValidator) internal {
41+
address oldValidator = TokenTransferValidatorStorage.layout()._transferValidator;
42+
if (oldValidator == newValidator) {
43+
revert SameTransferValidator();
44+
}
45+
TokenTransferValidatorStorage.layout()._transferValidator = newValidator;
46+
emit TransferValidatorUpdated(oldValidator, newValidator);
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.17;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {TestPlus} from "solady/test/utils/TestPlus.sol";
6+
import {Ownable} from "solady/src/auth/Ownable.sol";
7+
import {ICreatorToken} from "src/interfaces/transfer-validated/ICreatorToken.sol";
8+
import {ITransferValidator1155} from "src/interfaces/transfer-validated/ITransferValidator.sol";
9+
import {MockTransferValidator} from "./mock/MockTransferValidator.sol";
10+
import {ERC1155ShipyardTransferValidated} from "src/transfer-validated/ERC1155ShipyardTransferValidated.sol";
11+
12+
contract ERC1155ShipyardTransferValidatedWithMint is ERC1155ShipyardTransferValidated {
13+
constructor(address initialTransferValidator) ERC1155ShipyardTransferValidated(initialTransferValidator) {}
14+
15+
function mint(address to, uint256 id, uint256 amount) public onlyOwner {
16+
_mint(to, id, amount, "");
17+
}
18+
}
19+
20+
contract TestERC1155ShipyardTransferValidated is Test, TestPlus {
21+
MockTransferValidator transferValidatorAlwaysSucceeds = new MockTransferValidator(false);
22+
MockTransferValidator transferValidatorAlwaysReverts = new MockTransferValidator(true);
23+
24+
event TransferValidatorUpdated(address oldValidator, address newValidator);
25+
26+
ERC1155ShipyardTransferValidatedWithMint token;
27+
28+
function setUp() public {
29+
token = new ERC1155ShipyardTransferValidatedWithMint(address(0));
30+
}
31+
32+
function testOnlyOwnerCanSetTransferValidator() public {
33+
assertEq(token.getTransferValidator(), address(0));
34+
35+
vm.prank(address(token));
36+
vm.expectRevert(Ownable.Unauthorized.selector);
37+
token.setTransferValidator(address(transferValidatorAlwaysSucceeds));
38+
39+
token.setTransferValidator(address(transferValidatorAlwaysSucceeds));
40+
assertEq(token.getTransferValidator(), address(transferValidatorAlwaysSucceeds));
41+
}
42+
43+
function testTransferValidatedSetInConstructor() public {
44+
ERC1155ShipyardTransferValidatedWithMint token2 =
45+
new ERC1155ShipyardTransferValidatedWithMint(address(transferValidatorAlwaysSucceeds));
46+
47+
assertEq(token2.getTransferValidator(), address(transferValidatorAlwaysSucceeds));
48+
}
49+
50+
function testTransferValidatorIsCalledOnTransfer() public {
51+
token.mint(address(this), 1, 10);
52+
token.mint(address(this), 2, 10);
53+
54+
vm.expectEmit(true, true, true, true);
55+
emit TransferValidatorUpdated(address(0), address(transferValidatorAlwaysSucceeds));
56+
token.setTransferValidator(address(transferValidatorAlwaysSucceeds));
57+
token.safeTransferFrom(address(this), msg.sender, 1, 1, "");
58+
uint256[] memory ids = new uint256[](2);
59+
uint256[] memory amounts = new uint256[](2);
60+
ids[0] = 1;
61+
ids[1] = 2;
62+
amounts[0] = 2;
63+
amounts[1] = 2;
64+
token.safeBatchTransferFrom(address(this), msg.sender, ids, amounts, "");
65+
66+
vm.expectEmit(true, true, true, true);
67+
emit TransferValidatorUpdated(address(transferValidatorAlwaysSucceeds), address(transferValidatorAlwaysReverts));
68+
token.setTransferValidator(address(transferValidatorAlwaysReverts));
69+
vm.expectRevert("MockTransferValidator: always reverts");
70+
token.safeTransferFrom(address(this), msg.sender, 1, 1, "");
71+
vm.expectRevert("MockTransferValidator: always reverts");
72+
token.safeBatchTransferFrom(address(this), msg.sender, ids, amounts, "");
73+
74+
// When set to null address, transfer should succeed without calling the validator
75+
vm.expectEmit(true, true, true, true);
76+
emit TransferValidatorUpdated(address(transferValidatorAlwaysReverts), address(0));
77+
token.setTransferValidator(address(0));
78+
token.safeTransferFrom(address(this), msg.sender, 1, 1, "");
79+
token.safeBatchTransferFrom(address(this), msg.sender, ids, amounts, "");
80+
}
81+
82+
function testGetTransferValidationFunction() public {
83+
(bytes4 functionSignature, bool isViewFunction) = token.getTransferValidationFunction();
84+
assertEq(functionSignature, ITransferValidator1155.validateTransfer.selector);
85+
assertEq(isViewFunction, true);
86+
}
87+
88+
function testSupportsInterface() public {
89+
assertEq(token.supportsInterface(type(ICreatorToken).interfaceId), true);
90+
}
91+
92+
function onERC1155Received(
93+
address, /* operator */
94+
address, /* from */
95+
uint256, /* id */
96+
uint256, /* value */
97+
bytes calldata /* data */
98+
) external pure returns (bytes4) {
99+
return this.onERC1155Received.selector;
100+
}
101+
}

0 commit comments

Comments
 (0)