Skip to content

Commit

Permalink
add transfer validator contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanio committed Apr 8, 2024
1 parent 6077378 commit c425dbd
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 0 deletions.
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;
}
77 changes: 77 additions & 0 deletions src/transfer-validated/ERC1155ShipyardTransferValidated.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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} 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 {
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 = _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 "";
}
}
64 changes: 64 additions & 0 deletions src/transfer-validated/ERC721ShipyardTransferValidated.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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} 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 {
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 = _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 "";
}
}
33 changes: 33 additions & 0 deletions src/transfer-validated/lib/TokenTransferValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

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

/**
* @title TokenTransferValidator
* @notice Functionality to use a transfer validator.
*/
abstract contract TokenTransferValidator is ICreatorToken {
/// @dev Store the transfer validator. The null address means no transfer validator is set.
address internal _transferValidator;

/// @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 _transferValidator;
}

/// @notice Set the transfer validator.
/// The external method that uses this must include access control.
function _setTransferValidator(address newValidator) internal {
address oldValidator = _transferValidator;
if (oldValidator == newValidator) {
revert SameTransferValidator();
}
_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;
}
}
81 changes: 81 additions & 0 deletions test/transfer-validated/ERC721ShipyardTransferValidated.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 {ITransferValidator721} from "src/interfaces/transfer-validated/ITransferValidator.sol";
import {MockTransferValidator} from "./mock/MockTransferValidator.sol";
import {ERC721ShipyardTransferValidated} from "src/transfer-validated/ERC721ShipyardTransferValidated.sol";

contract ERC721ShipyardTransferValidatedWithMint is ERC721ShipyardTransferValidated {
constructor(address initialTransferValidator) ERC721ShipyardTransferValidated(initialTransferValidator) {}

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

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

event TransferValidatorUpdated(address oldValidator, address newValidator);

ERC721ShipyardTransferValidatedWithMint token;

function setUp() public {
token = new ERC721ShipyardTransferValidatedWithMint(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 {
ERC721ShipyardTransferValidatedWithMint token2 =
new ERC721ShipyardTransferValidatedWithMint(address(transferValidatorAlwaysSucceeds));

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

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

vm.expectEmit(true, true, true, true);
emit TransferValidatorUpdated(address(0), address(transferValidatorAlwaysSucceeds));
token.setTransferValidator(address(transferValidatorAlwaysSucceeds));
token.safeTransferFrom(address(this), msg.sender, 1);

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, 2);

// 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, 2);
}

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

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

0 comments on commit c425dbd

Please sign in to comment.