diff --git a/contracts/GlobalMaker.sol b/contracts/GlobalMaker.sol new file mode 100644 index 0000000..06bd600 --- /dev/null +++ b/contracts/GlobalMaker.sol @@ -0,0 +1,63 @@ +/* + + << Global Maker >> + +*/ + +pragma solidity 0.7.5; + +import "./lib/EIP1271Mod.sol"; +import "./lib/ArrayUtils.sol"; +import "./registry/ProxyRegistry.sol"; + +/** + * @title GlobalMaker + * @author Wyvern Protocol Developers + */ +contract GlobalMaker is ERC1271Mod { + + bytes4 constant internal SIGINVALID = 0x00000000; + + string public constant name = "Global Maker"; + + /** + * Construct a new GlobalMaker, creating the proxy it will require + */ + constructor (ProxyRegistry registry) public { + registry.registerProxy(); + } + + /** + * Check if a signature is valid + * + * @param _data Data signed over + * @param _signature Encoded signature + * @return magicValue Magic value if valid, zero-value otherwise + */ + function isValidSignature( + bytes memory _data, + bytes memory _signature, + bytes memory _extradata) + override + public + view + returns (bytes4 magicValue) + { + // assert signature + bytes memory sig = ArrayUtils.arrayTake(abi.encodeWithSignature("transferFrom(address,address,uint256)"), 4); + require(ArrayUtils.arrayEq(sig, ArrayUtils.arrayTake(_extradata, 4))); + + // decode calldata + (address callFrom, address callTo, uint256 token) = abi.decode(ArrayUtils.arrayDrop(_extradata, 4), (address, address, uint256)); + + // check that the user (whoever is sending the tokens) signed the order hash + bytes32 hash = abi.decode(_data, (bytes32)); + (uint8 v, bytes32 r, bytes32 s) = abi.decode(_signature, (uint8, bytes32, bytes32)); + if (callFrom == ecrecover(hash, v, r, s)) { + return MAGICVALUE; + } else { + return SIGINVALID; + } + } + +} diff --git a/contracts/exchange/Exchange.sol b/contracts/exchange/Exchange.sol index e000a40..1260bba 100644 --- a/contracts/exchange/Exchange.sol +++ b/contracts/exchange/Exchange.sol @@ -41,14 +41,6 @@ contract Exchange is ExchangeCore { return validateOrderParameters(order, hashOrder(order)); } - function validateOrderAuthorization_(bytes32 hash, address maker, bytes calldata signature) - external - view - returns (bool) - { - return validateOrderAuthorization(hash, maker, signature); - } - function approveOrderHash_(bytes32 hash) external { diff --git a/contracts/exchange/ExchangeCore.sol b/contracts/exchange/ExchangeCore.sol index 2406878..93a0113 100644 --- a/contracts/exchange/ExchangeCore.sol +++ b/contracts/exchange/ExchangeCore.sol @@ -12,6 +12,7 @@ import "../lib/StaticCaller.sol"; import "../lib/ReentrancyGuarded.sol"; import "../lib/EIP712.sol"; import "../lib/EIP1271.sol"; +import "../lib/EIP1271Mod.sol"; import "../registry/ProxyRegistryInterface.sol"; import "../registry/AuthenticatedProxy.sol"; @@ -153,7 +154,7 @@ contract ExchangeCore is ReentrancyGuarded, StaticCaller, EIP712 { return true; } - function validateOrderAuthorization(bytes32 hash, address maker, bytes memory signature) + function validateOrderAuthorization(bytes32 hash, address maker, bytes memory signature, Call memory call) internal view returns (bool) @@ -184,6 +185,10 @@ contract ExchangeCore is ReentrancyGuarded, StaticCaller, EIP712 { /* (c): Contract-only authentication: EIP/ERC 1271. */ if (isContract) { + if (ERC1271Mod(maker).isValidSignature(abi.encodePacked(calculatedHashToSign), signature, call.data) == EIP_1271_MAGICVALUE) { + return true; + } + if (ERC1271(maker).isValidSignature(abi.encodePacked(calculatedHashToSign), signature) == EIP_1271_MAGICVALUE) { return true; } @@ -321,10 +326,10 @@ contract ExchangeCore is ReentrancyGuarded, StaticCaller, EIP712 { (bytes memory firstSignature, bytes memory secondSignature) = abi.decode(signatures, (bytes, bytes)); /* Check first order authorization. */ - require(validateOrderAuthorization(firstHash, firstOrder.maker, firstSignature), "First order failed authorization"); + require(validateOrderAuthorization(firstHash, firstOrder.maker, firstSignature, firstCall), "First order failed authorization"); /* Check second order authorization. */ - require(validateOrderAuthorization(secondHash, secondOrder.maker, secondSignature), "Second order failed authorization"); + require(validateOrderAuthorization(secondHash, secondOrder.maker, secondSignature, secondCall), "Second order failed authorization"); } /* INTERACTIONS */ @@ -358,14 +363,14 @@ contract ExchangeCore is ReentrancyGuarded, StaticCaller, EIP712 { /* EFFECTS */ - /* Update first order fill, if necessary. */ + /* Update first order fill, if necessary. When the sender is the maker, this is not done in order to save gas. */ if (firstOrder.maker != msg.sender) { if (firstFill != previousFirstFill) { fills[firstOrder.maker][firstHash] = firstFill; } } - /* Update second order fill, if necessary. */ + /* Update second order fill, if necessary. When the sender is the maker, this is not done in order to save gas. */ if (secondOrder.maker != msg.sender) { if (secondFill != previousSecondFill) { fills[secondOrder.maker][secondHash] = secondFill; diff --git a/contracts/lib/EIP1271Mod.sol b/contracts/lib/EIP1271Mod.sol new file mode 100644 index 0000000..b14e819 --- /dev/null +++ b/contracts/lib/EIP1271Mod.sol @@ -0,0 +1,32 @@ +/* + + << EIP 1271 (modified) >> + +*/ + +pragma solidity 0.7.5; + +abstract contract ERC1271Mod { + + // bytes4(keccak256("isValidSignature(bytes,bytes)") + bytes4 constant internal MAGICVALUE = 0x20c13b0b; + + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param _data Arbitrary length data signed on the behalf of address(this) + * @param _signature Signature byte array associated with _data + * @param _extradata Extra data which may be relevant to validation + * + * MUST return the bytes4 magic value 0x20c13b0b when function passes. + * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5) + * MUST allow external calls + */ + function isValidSignature( + bytes memory _data, + bytes memory _signature, + bytes memory _extradata) + virtual + public + view + returns (bytes4 magicValue); +} diff --git a/migrations/3_wyvern_registry_and_exchange.js b/migrations/3_wyvern_registry_and_exchange.js index 39e3dfb..a53cc67 100644 --- a/migrations/3_wyvern_registry_and_exchange.js +++ b/migrations/3_wyvern_registry_and_exchange.js @@ -2,6 +2,7 @@ const WyvernRegistry = artifacts.require('./WyvernRegistry.sol') const WyvernExchange = artifacts.require('./WyvernExchange.sol') +const GlobalMaker = artifacts.require('./GlobalMaker.sol') const { setConfig } = require('./config.js') const chainIds = { @@ -18,7 +19,11 @@ module.exports = (deployer, network) => { return deployer.deploy(WyvernExchange, chainIds[network], [WyvernRegistry.address, '0xa5409ec958C83C3f309868babACA7c86DCB077c1']).then(() => { if (network !== 'development') setConfig('deployed.' + network + '.WyvernExchange', WyvernExchange.address) return WyvernRegistry.deployed().then(registry => { - return registry.grantInitialAuthentication(WyvernExchange.address) + return registry.grantInitialAuthentication(WyvernExchange.address).then(() => { + return deployer.deploy(GlobalMaker, WyvernRegistry.address).then(() => { + if (network !== 'development') setConfig('deployed.' + network + '.GlobalMaker', GlobalMaker.address) + }) + }) }) }) }) diff --git a/test/7-global-maker-matching.js b/test/7-global-maker-matching.js new file mode 100755 index 0000000..6aba8c1 --- /dev/null +++ b/test/7-global-maker-matching.js @@ -0,0 +1,86 @@ +/* global artifacts:false, it:false, contract:false, assert:false */ + +const WyvernAtomicizer = artifacts.require('WyvernAtomicizer') +const WyvernExchange = artifacts.require('WyvernExchange') +const WyvernStatic = artifacts.require('WyvernStatic') +const WyvernRegistry = artifacts.require('WyvernRegistry') +const TestERC1155 = artifacts.require('TestERC1155') +const TestERC1271 = artifacts.require('TestERC1271') +const TestERC20 = artifacts.require('TestERC20') +const TestERC721 = artifacts.require('TestERC721') +const GlobalMaker = artifacts.require('GlobalMaker') + +const Web3 = require('web3') +const provider = new Web3.providers.HttpProvider('http://localhost:8545') +const web3 = new Web3(provider) + +const {wrap,hashOrder,ZERO_BYTES32,randomUint,NULL_SIG,assertIsRejected} = require('./aux') + +const CHAIN_ID = 50 + +contract('WyvernExchange', (accounts) => + { + let deploy_core_contracts = async () => + { + let [registry,atomicizer] = await Promise.all([WyvernRegistry.new(), WyvernAtomicizer.new(), GlobalMaker.new()]) + let [exchange,statici] = await Promise.all([WyvernExchange.new(CHAIN_ID,[registry.address]),WyvernStatic.new(atomicizer.address)]) + await registry.grantInitialAuthentication(exchange.address) + return {registry,exchange:wrap(exchange),atomicizer,statici} + } + + let deploy = async contracts => Promise.all(contracts.map(contract => contract.new())) + + it('matches erc1155 nft-nft swap order',async () => + { + let account_a = accounts[0] + let account_b = accounts[6] + + let {exchange, registry, statici} = await deploy_core_contracts() + let [erc1155, maker] = await deploy([TestERC1155, GlobalMaker]) + + await registry.registerProxy({from: account_a}) + let proxy1 = await registry.proxies(account_a) + assert.equal(true, proxy1.length > 0, 'no proxy address for account a') + + let proxy2 = await registry.proxies(maker.address) + assert.equal(true, proxy2.length > 0, 'no proxy address for global maker') + + // what to do about approval from the maker contract + // should be the user account approving + + await Promise.all([erc1155.setApprovalForAll(proxy1,true,{from: account_a}),erc1155.setApprovalForAll(proxy2,true,{from: account_b})]) + + let nfts = [{tokenId:4,amount:1},{tokenId:5,amount:1}] + await Promise.all([erc1155.mint(account_a,nfts[0].tokenId),erc1155.mint(account_b,nfts[1].tokenId)]) + + const erc1155c = new web3.eth.Contract(erc1155.abi, erc1155.address) + const selector = web3.eth.abi.encodeFunctionSignature('swapOneForOneERC1155(bytes,address[7],uint8[2],uint256[6],bytes,bytes)') + + const paramsOne = web3.eth.abi.encodeParameters( + ['address[2]', 'uint256[2]', 'uint256[2]'], + [[erc1155.address, erc1155.address], [nfts[0].tokenId, nfts[1].tokenId], [nfts[0].amount, nfts[1].amount]] + ) + + const paramsTwo = web3.eth.abi.encodeParameters( + ['address[2]', 'uint256[2]', 'uint256[2]'], + [[erc1155.address, erc1155.address], [nfts[1].tokenId, nfts[0].tokenId], [nfts[1].amount, nfts[0].amount]] + ) + + const one = {registry: registry.address, maker: account_a, staticTarget: statici.address, staticSelector: selector, staticExtradata: paramsOne, maximumFill: '1', listingTime: '0', expirationTime: '10000000000', salt: '7'} + const two = {registry: registry.address, maker: account_b, staticTarget: statici.address, staticSelector: selector, staticExtradata: paramsTwo, maximumFill: '1', listingTime: '0', expirationTime: '10000000000', salt: '8'} + + const firstData = erc1155c.methods.safeTransferFrom(account_a, account_b, nfts[0].tokenId, nfts[0].amount, "0x").encodeABI() + ZERO_BYTES32.substr(2) + const secondData = erc1155c.methods.safeTransferFrom(account_b, account_a, nfts[1].tokenId, nfts[1].amount, "0x").encodeABI() + ZERO_BYTES32.substr(2) + + const firstCall = {target: erc1155.address, howToCall: 0, data: firstData} + const secondCall = {target: erc1155.address, howToCall: 0, data: secondData} + const sigOne = NULL_SIG + let sigTwo = await exchange.sign(two, account_b) + + await exchange.atomicMatch(one, sigOne, firstCall, two, sigTwo, secondCall, ZERO_BYTES32) + let [new_balance1,new_balance2] = await Promise.all([erc1155.balanceOf(account_a, nfts[1].tokenId),erc1155.balanceOf(account_b, nfts[0].tokenId)]) + assert.isTrue(new_balance1.toNumber() > 0,'Incorrect owner') + assert.isTrue(new_balance2.toNumber() > 0,'Incorrect owner') + }) + + })