-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from ProjectOpenSea/ryan/add-transfer-validator
add transfer validator contracts
- Loading branch information
Showing
10 changed files
with
440 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
src/transfer-validated/ERC1155ShipyardTransferValidated.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
66
src/transfer-validated/ERC721ShipyardTransferValidated.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ""; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
101
test/transfer-validated/ERC1155ShipyardTransferValidated.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.