Skip to content

Commit

Permalink
Merge pull request #23 from ProjectOpenSea/ryan/add-transfer-validator
Browse files Browse the repository at this point in the history
add transfer validator contracts
  • Loading branch information
ryanio authored Apr 8, 2024
2 parents 6077378 + fee74c5 commit 85a6df1
Show file tree
Hide file tree
Showing 10 changed files with 440 additions and 2 deletions.
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
# Github Actions automatically updated formatting with forge fmt\n2d96311b29055c5b9a0b632176ce3b8d78a23a89
# Github Actions automatically updated formatting with forge fmt\ne2d336bbb0331c7716c16fed64f51be6c270ad02
# Github Actions automatically updated formatting with forge fmt\n3458423c8716bdcd3875cde4597dcae377a16620
# Github Actions automatically updated formatting with forge fmt\n2ee13e093c7f5aee04944b79dadcf16089cbb560
12 changes: 12 additions & 0 deletions src/interfaces/transfer-validated/ICreatorToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

interface ICreatorToken {
event TransferValidatorUpdated(address oldValidator, address newValidator);

function getTransferValidator() external view returns (address validator);

function getTransferValidationFunction() external view returns (bytes4 functionSignature, bool isViewFunction);

function setTransferValidator(address validator) external;
}
12 changes: 12 additions & 0 deletions src/interfaces/transfer-validated/ITransferValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

interface ITransferValidator721 {
/// @notice Ensure that a transfer has been authorized for a specific tokenId
function validateTransfer(address caller, address from, address to, uint256 tokenId) external view;
}

interface ITransferValidator1155 {
/// @notice Ensure that a transfer has been authorized for a specific amount of a specific tokenId, and reduce the transferable amount remaining
function validateTransfer(address caller, address from, address to, uint256 tokenId, uint256 amount) external;
}
4 changes: 2 additions & 2 deletions src/reference/ExampleNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ contract ExampleNFT is AbstractNFT {
svg.prop("text-anchor", "middle"),
svg.prop("font-size", "48"),
svg.prop("fill", "black")
),
),
children: LibString.toString(tokenId)
})
)
)
});
}

Expand Down
79 changes: 79 additions & 0 deletions src/transfer-validated/ERC1155ShipyardTransferValidated.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {Ownable} from "solady/src/auth/Ownable.sol";
import {ERC1155} from "solady/src/tokens/ERC1155.sol";
import {ERC1155ConduitPreapproved_Solady} from "../tokens/erc1155/ERC1155ConduitPreapproved_Solady.sol";
import {TokenTransferValidator, TokenTransferValidatorStorage} from "./lib/TokenTransferValidator.sol";
import {ICreatorToken} from "../interfaces/transfer-validated/ICreatorToken.sol";
import {ITransferValidator1155} from "../interfaces/transfer-validated/ITransferValidator.sol";

contract ERC1155ShipyardTransferValidated is ERC1155ConduitPreapproved_Solady, TokenTransferValidator, Ownable {
using TokenTransferValidatorStorage for TokenTransferValidatorStorage.Layout;

constructor(address initialTransferValidator) ERC1155ConduitPreapproved_Solady() {
// Set the initial contract owner.
_initializeOwner(msg.sender);

// Set the initial transfer validator.
if (initialTransferValidator != address(0)) {
_setTransferValidator(initialTransferValidator);
}
}

/// @notice Returns the transfer validation function used.
function getTransferValidationFunction() external pure returns (bytes4 functionSignature, bool isViewFunction) {
functionSignature = ITransferValidator1155.validateTransfer.selector;
isViewFunction = true;
}

/// @notice Set the transfer validator. Only callable by the token owner.
function setTransferValidator(address newValidator) external onlyOwner {
// Set the new transfer validator.
_setTransferValidator(newValidator);
}

/// @dev Override this function to return true if `_beforeTokenTransfer` is used.
function _useBeforeTokenTransfer() internal view virtual override returns (bool) {
return true;
}

/// @dev Hook that is called before any token transfer. This includes minting and burning.
function _beforeTokenTransfer(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory /* data */
) internal virtual override {
if (from != address(0) && to != address(0)) {
// Call the transfer validator if one is set.
address transferValidator = TokenTransferValidatorStorage.layout()._transferValidator;
if (transferValidator != address(0)) {
for (uint256 i = 0; i < ids.length; i++) {
ITransferValidator1155(transferValidator).validateTransfer(msg.sender, from, to, ids[i], amounts[i]);
}
}
}
}

/// @dev Override supportsInterface to additionally return true for ICreatorToken.
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155) returns (bool) {
return interfaceId == type(ICreatorToken).interfaceId || ERC1155.supportsInterface(interfaceId);
}

/// @dev Replace me with the token name.
function name() public view virtual returns (string memory) {
return "ERC1155ShipyardTransferValidated";
}

/// @dev Replace me with the token symbol.
function symbol() public view virtual returns (string memory) {
return "ERC1155-S-TV";
}

/// @dev Replace me with the token URI.
function uri(uint256 /* id */ ) public view virtual override returns (string memory) {
return "";
}
}
66 changes: 66 additions & 0 deletions src/transfer-validated/ERC721ShipyardTransferValidated.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {Ownable} from "solady/src/auth/Ownable.sol";
import {ERC721} from "solady/src/tokens/ERC721.sol";
import {ERC721ConduitPreapproved_Solady} from "../tokens/erc721/ERC721ConduitPreapproved_Solady.sol";
import {TokenTransferValidator, TokenTransferValidatorStorage} from "./lib/TokenTransferValidator.sol";
import {ICreatorToken} from "../interfaces/transfer-validated/ICreatorToken.sol";
import {ITransferValidator721} from "../interfaces/transfer-validated/ITransferValidator.sol";

contract ERC721ShipyardTransferValidated is ERC721ConduitPreapproved_Solady, TokenTransferValidator, Ownable {
using TokenTransferValidatorStorage for TokenTransferValidatorStorage.Layout;

constructor(address initialTransferValidator) ERC721ConduitPreapproved_Solady() {
// Set the initial contract owner.
_initializeOwner(msg.sender);

// Set the initial transfer validator.
if (initialTransferValidator != address(0)) {
_setTransferValidator(initialTransferValidator);
}
}

/// @notice Returns the transfer validation function used.
function getTransferValidationFunction() external pure returns (bytes4 functionSignature, bool isViewFunction) {
functionSignature = ITransferValidator721.validateTransfer.selector;
isViewFunction = false;
}

/// @notice Set the transfer validator. Only callable by the token owner.
function setTransferValidator(address newValidator) external onlyOwner {
// Set the new transfer validator.
_setTransferValidator(newValidator);
}

/// @dev Hook that is called before any token transfer. This includes minting and burning.
function _beforeTokenTransfer(address from, address to, uint256 id) internal virtual override {
if (from != address(0) && to != address(0)) {
// Call the transfer validator if one is set.
address transferValidator = TokenTransferValidatorStorage.layout()._transferValidator;
if (transferValidator != address(0)) {
ITransferValidator721(transferValidator).validateTransfer(msg.sender, from, to, id);
}
}
}

/// @dev Override supportsInterface to additionally return true for ICreatorToken.
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) {
return interfaceId == type(ICreatorToken).interfaceId || ERC721.supportsInterface(interfaceId);
}

/// @dev Replace me with the token name.
function name() public view virtual override returns (string memory) {
return "ERC721ShipyardTransferValidated";
}

/// @dev Replace me with the token symbol.
function symbol() public view virtual override returns (string memory) {
return "ERC721-S-TV";
}

/// @dev Replace me with the token URI.
function tokenURI(uint256 /* id */ ) public view virtual override returns (string memory) {
return "";
}
}
48 changes: 48 additions & 0 deletions src/transfer-validated/lib/TokenTransferValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {ICreatorToken} from "../../interfaces/transfer-validated/ICreatorToken.sol";

library TokenTransferValidatorStorage {
struct Layout {
/// @dev Store the transfer validator. The null address means no transfer validator is set.
address _transferValidator;
}

bytes32 internal constant STORAGE_SLOT = keccak256("contracts.storage.tokenTransferValidator");

function layout() internal pure returns (Layout storage l) {
bytes32 slot = STORAGE_SLOT;
assembly {
l.slot := slot
}
}
}

/**
* @title TokenTransferValidator
* @notice Functionality to use a transfer validator.
*/
abstract contract TokenTransferValidator is ICreatorToken {
using TokenTransferValidatorStorage for TokenTransferValidatorStorage.Layout;

/// @notice Revert with an error if the transfer validator is being set to the same address.
error SameTransferValidator();

/// @notice Returns the currently active transfer validator.
/// The null address means no transfer validator is set.
function getTransferValidator() external view returns (address) {
return TokenTransferValidatorStorage.layout()._transferValidator;
}

/// @notice Set the transfer validator.
/// The external method that uses this must include access control.
function _setTransferValidator(address newValidator) internal {
address oldValidator = TokenTransferValidatorStorage.layout()._transferValidator;
if (oldValidator == newValidator) {
revert SameTransferValidator();
}
TokenTransferValidatorStorage.layout()._transferValidator = newValidator;
emit TransferValidatorUpdated(oldValidator, newValidator);
}
}
101 changes: 101 additions & 0 deletions test/transfer-validated/ERC1155ShipyardTransferValidated.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {Test} from "forge-std/Test.sol";
import {TestPlus} from "solady/test/utils/TestPlus.sol";
import {Ownable} from "solady/src/auth/Ownable.sol";
import {ICreatorToken} from "src/interfaces/transfer-validated/ICreatorToken.sol";
import {ITransferValidator1155} from "src/interfaces/transfer-validated/ITransferValidator.sol";
import {MockTransferValidator} from "./mock/MockTransferValidator.sol";
import {ERC1155ShipyardTransferValidated} from "src/transfer-validated/ERC1155ShipyardTransferValidated.sol";

contract ERC1155ShipyardTransferValidatedWithMint is ERC1155ShipyardTransferValidated {
constructor(address initialTransferValidator) ERC1155ShipyardTransferValidated(initialTransferValidator) {}

function mint(address to, uint256 id, uint256 amount) public onlyOwner {
_mint(to, id, amount, "");
}
}

contract TestERC1155ShipyardTransferValidated is Test, TestPlus {
MockTransferValidator transferValidatorAlwaysSucceeds = new MockTransferValidator(false);
MockTransferValidator transferValidatorAlwaysReverts = new MockTransferValidator(true);

event TransferValidatorUpdated(address oldValidator, address newValidator);

ERC1155ShipyardTransferValidatedWithMint token;

function setUp() public {
token = new ERC1155ShipyardTransferValidatedWithMint(address(0));
}

function testOnlyOwnerCanSetTransferValidator() public {
assertEq(token.getTransferValidator(), address(0));

vm.prank(address(token));
vm.expectRevert(Ownable.Unauthorized.selector);
token.setTransferValidator(address(transferValidatorAlwaysSucceeds));

token.setTransferValidator(address(transferValidatorAlwaysSucceeds));
assertEq(token.getTransferValidator(), address(transferValidatorAlwaysSucceeds));
}

function testTransferValidatedSetInConstructor() public {
ERC1155ShipyardTransferValidatedWithMint token2 =
new ERC1155ShipyardTransferValidatedWithMint(address(transferValidatorAlwaysSucceeds));

assertEq(token2.getTransferValidator(), address(transferValidatorAlwaysSucceeds));
}

function testTransferValidatorIsCalledOnTransfer() public {
token.mint(address(this), 1, 10);
token.mint(address(this), 2, 10);

vm.expectEmit(true, true, true, true);
emit TransferValidatorUpdated(address(0), address(transferValidatorAlwaysSucceeds));
token.setTransferValidator(address(transferValidatorAlwaysSucceeds));
token.safeTransferFrom(address(this), msg.sender, 1, 1, "");
uint256[] memory ids = new uint256[](2);
uint256[] memory amounts = new uint256[](2);
ids[0] = 1;
ids[1] = 2;
amounts[0] = 2;
amounts[1] = 2;
token.safeBatchTransferFrom(address(this), msg.sender, ids, amounts, "");

vm.expectEmit(true, true, true, true);
emit TransferValidatorUpdated(address(transferValidatorAlwaysSucceeds), address(transferValidatorAlwaysReverts));
token.setTransferValidator(address(transferValidatorAlwaysReverts));
vm.expectRevert("MockTransferValidator: always reverts");
token.safeTransferFrom(address(this), msg.sender, 1, 1, "");
vm.expectRevert("MockTransferValidator: always reverts");
token.safeBatchTransferFrom(address(this), msg.sender, ids, amounts, "");

// When set to null address, transfer should succeed without calling the validator
vm.expectEmit(true, true, true, true);
emit TransferValidatorUpdated(address(transferValidatorAlwaysReverts), address(0));
token.setTransferValidator(address(0));
token.safeTransferFrom(address(this), msg.sender, 1, 1, "");
token.safeBatchTransferFrom(address(this), msg.sender, ids, amounts, "");
}

function testGetTransferValidationFunction() public {
(bytes4 functionSignature, bool isViewFunction) = token.getTransferValidationFunction();
assertEq(functionSignature, ITransferValidator1155.validateTransfer.selector);
assertEq(isViewFunction, true);
}

function testSupportsInterface() public {
assertEq(token.supportsInterface(type(ICreatorToken).interfaceId), true);
}

function onERC1155Received(
address, /* operator */
address, /* from */
uint256, /* id */
uint256, /* value */
bytes calldata /* data */
) external pure returns (bytes4) {
return this.onERC1155Received.selector;
}
}
Loading

0 comments on commit 85a6df1

Please sign in to comment.