wyvernprotocol / wyvern-v3

Wyvern Protocol v3.1, Ethereum implementation
https://wyvernprotocol.com
MIT License
298 stars 121 forks source link

Integrating fees #56

Closed georgeroman closed 3 years ago

georgeroman commented 3 years ago

Hello everyone! I'm looking into integrating fees within the orders. That is, the maker of an order involving selling an ERC20 asset is responsible for paying a list of fee recipients - these must be specified beforehand and signed by the maker so that they're enforced on-chain. I'm having a hard time composing the proper predicates for this. Are there any concrete examples of this?

georgeroman commented 3 years ago

Ok, for anyone interested in how this worked out in the end, below is a sample code that works. However, it involves a new predicate transferERC20ExactTo which gets the fee recipient via the extra data.

Click for code ```typescript it("erc721 <> erc20 with checks", async () => { const alice = accounts[0]; const bob = accounts[1]; const carol = accounts[2]; const david = accounts[3]; const { atomicizer, exchange, registry, statici } = await deployCoreContracts(); const [erc20, erc721] = await deploy([TestERC20, TestERC721]); const abi = [ { constant: false, inputs: [ { name: "addrs", type: "address[]" }, { name: "values", type: "uint256[]" }, { name: "calldataLengths", type: "uint256[]" }, { name: "calldatas", type: "bytes" }, ], name: "atomicize", outputs: [], payable: false, stateMutability: "nonpayable", type: "function", }, ]; const atomicizerc = new web3.eth.Contract(abi, atomicizer.address); await registry.registerProxy({ from: alice }); const aliceProxy = await registry.proxies(alice); assert.equal(true, aliceProxy.length > 0, "No proxy address for Alice"); await registry.registerProxy({ from: bob }); const bobProxy = await registry.proxies(bob); assert.equal(true, bobProxy.length > 0, "No proxy address for Bob"); const amount = 1000; const fee1 = 10; const fee2 = 20; const tokenId = 0; await Promise.all([ erc20.mint(bob, amount + fee1 + fee2), erc721.mint(alice, tokenId), ]); await Promise.all([ erc20.approve(bobProxy, amount + fee1 + fee2, { from: bob }), erc721.setApprovalForAll(aliceProxy, true, { from: alice }), ]); const erc20c = new web3.eth.Contract(erc20.abi, erc20.address); const erc721c = new web3.eth.Contract(erc721.abi, erc721.address); let selectorOne, extradataOne; { const selector = web3.eth.abi.encodeFunctionSignature( "split(bytes,address[7],uint8[2],uint256[6],bytes,bytes)" ); // Call should be an ERC721 transfer const selectorCall = web3.eth.abi.encodeFunctionSignature( "transferERC721Exact(bytes,address[7],uint8,uint256[6],bytes)" ); const extradataCall = web3.eth.abi.encodeParameters( ["address", "uint256"], [erc721.address, tokenId] ); // Countercall should include an ERC20 transfer const selectorCountercall = web3.eth.abi.encodeFunctionSignature( "sequenceAnyAfter(bytes,address[7],uint8,uint256[6],bytes)" ); const countercallSelector1 = web3.eth.abi.encodeFunctionSignature( "transferERC20Exact(bytes,address[7],uint8,uint256[6],bytes)" ); const countercallExtradata1 = web3.eth.abi.encodeParameters( ["address", "uint256"], [erc20.address, amount] ); const extradataCountercall = web3.eth.abi.encodeParameters( ["address[]", "uint256[]", "bytes4[]", "bytes"], [ [statici.address], [(countercallExtradata1.length - 2) / 2], [countercallSelector1], countercallExtradata1, ] ); const params = web3.eth.abi.encodeParameters( ["address[2]", "bytes4[2]", "bytes", "bytes"], [ [statici.address, statici.address], [selectorCall, selectorCountercall], extradataCall, extradataCountercall, ] ); selectorOne = selector; extradataOne = params; } const one = { registry: registry.address, maker: alice, staticTarget: statici.address, staticSelector: selectorOne, staticExtradata: extradataOne, maximumFill: 1, listingTime: "0", expirationTime: "10000000000", salt: "11", }; const sigOne = await exchange.sign(one, alice); let selectorTwo, extradataTwo; { const selector = web3.eth.abi.encodeFunctionSignature( "split(bytes,address[7],uint8[2],uint256[6],bytes,bytes)" ); // Call should be an ERC20 transfer to recipient + fees const selectorCall = web3.eth.abi.encodeFunctionSignature( "sequenceExact(bytes,address[7],uint8,uint256[6],bytes)" ); const callSelector1 = web3.eth.abi.encodeFunctionSignature( "transferERC20Exact(bytes,address[7],uint8,uint256[6],bytes)" ); const callExtradata1 = web3.eth.abi.encodeParameters( ["address", "uint256"], [erc20.address, amount] ); const callSelector2 = web3.eth.abi.encodeFunctionSignature( "transferERC20ExactTo(bytes,address[7],uint8,uint256[6],bytes)" ); const callExtradata2 = web3.eth.abi.encodeParameters( ["address", "uint256", "address"], [erc20.address, fee1, carol] ); const callSelector3 = web3.eth.abi.encodeFunctionSignature( "transferERC20ExactTo(bytes,address[7],uint8,uint256[6],bytes)" ); const callExtradata3 = web3.eth.abi.encodeParameters( ["address", "uint256", "address"], [erc20.address, fee2, david] ); const extradataCall = web3.eth.abi.encodeParameters( ["address[]", "uint256[]", "bytes4[]", "bytes"], [ [statici.address, statici.address, statici.address], [ (callExtradata1.length - 2) / 2, (callExtradata2.length - 2) / 2, (callExtradata3.length - 2) / 2, ], [callSelector1, callSelector2, callSelector3], callExtradata1 + callExtradata2.slice("2") + callExtradata3.slice("2"), ] ); // Countercall should be an ERC721 transfer const selectorCountercall = web3.eth.abi.encodeFunctionSignature( "transferERC721Exact(bytes,address[7],uint8,uint256[6],bytes)" ); const extradataCountercall = web3.eth.abi.encodeParameters( ["address", "uint256"], [erc721.address, tokenId] ); const params = web3.eth.abi.encodeParameters( ["address[2]", "bytes4[2]", "bytes", "bytes"], [ [statici.address, statici.address], [selectorCall, selectorCountercall], extradataCall, extradataCountercall, ] ); selectorTwo = selector; extradataTwo = params; } const two = { registry: registry.address, maker: bob, staticTarget: statici.address, staticSelector: selectorTwo, staticExtradata: extradataTwo, maximumFill: amount, listingTime: "0", expirationTime: "10000000000", salt: "12", }; const sigTwo = await exchange.sign(two, bob); const firstData = erc721c.methods .transferFrom(alice, bob, tokenId) .encodeABI(); const c1 = erc20c.methods.transferFrom(bob, alice, amount).encodeABI(); const c2 = erc20c.methods.transferFrom(bob, carol, fee1).encodeABI(); const c3 = erc20c.methods.transferFrom(bob, david, fee2).encodeABI(); const secondData = atomicizerc.methods .atomicize( [erc20.address, erc20.address, erc20.address], [0, 0, 0], [(c1.length - 2) / 2, (c2.length - 2) / 2, (c3.length - 2) / 2], c1 + c2.slice("2") + c3.slice("2") ) .encodeABI(); const firstCall = { target: erc721.address, howToCall: 0, data: firstData }; const secondCall = { target: atomicizer.address, howToCall: 1, data: secondData, }; await exchange.atomicMatchWith( one, sigOne, firstCall, two, sigTwo, secondCall, ZERO_BYTES32, { from: carol } ); const [ aliceErc20Balance, carolErc20Balance, davidErc20Balance, tokenIdOwner, ] = await Promise.all([ erc20.balanceOf(alice), erc20.balanceOf(carol), erc20.balanceOf(david), erc721.ownerOf(tokenId), ]); assert.equal( aliceErc20Balance.toNumber(), amount, "Incorrect ERC20 balance" ); assert.equal(carolErc20Balance.toNumber(), fee1, "Incorrect ERC20 balance"); assert.equal(davidErc20Balance.toNumber(), fee2, "Incorrect ERC20 balance"); assert.equal(tokenIdOwner, bob, "Incorrect token owner"); }); ```
pranav3714 commented 2 years ago

Tried to execute the test came across this error: Error: Returned error: VM Exception while processing transaction: revert Static call failed -- Reason given: Static call failed. I know this issue was closed but unable to figure out a solution. @georgeroman

pranav3714 commented 2 years ago

Would the same function work for 1155 if i change the predicate functions

kr1p70n1c commented 2 years ago

@georgeroman I assume you had to add a Solidity function like the following in order to make it work:

    function transferERC20ExactTo(bytes memory extra,
        address[7] memory addresses, AuthenticatedProxy.HowToCall howToCall, uint[6] memory,
        bytes memory data)
    public
    pure
    {
...

correct?