wyvernprotocol / wyvern-v3

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

How about use `WyvernAtomicizer.atomicize` for eth transfer? #78

Closed smitea closed 2 years ago

smitea commented 2 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"); }); ```

Originally posted by @georgeroman in https://github.com/wyvernprotocol/wyvern-v3/issues/56#issuecomment-893203680

smitea commented 2 years ago

I'm looking for a solution like this. I want to use WyvernAtomicizer.atomicize for eth transfer (in the getTransferData function code), but it's sender is a proxy.


contract('WyvernExchange', (accounts) => {
  let deploy_core_contracts = async () => {
    let [registry, atomicizer] = await Promise.all([WyvernRegistry.new(), WyvernAtomicizer.new()])
    let [exchange, statici] = await Promise.all([WyvernExchange.new(CHAIN_ID, [registry.address], '0x'), 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()))

  const getTransferData = async (from, to, amount, coin) => {
    const value = amount
   // ERC20 transfer
    if (coin) {
      const erc20 = await getERC20(coin);
      const data = erc20.methods.transferFrom(from, to, value).encodeABI()
      return { data, addr: coin.addr, value: 0, len: (data.length - 2) / 2 }
    }
    // ETH transfer
    return { data: "0x", addr: to, value, len: 0, }
  }

  const getERC20 = async (coin) => {
    if (!coin) return null;
    let [erc20] = await deploy([TestERC20])
    return erc20;
  }

  const any_erc1155_for_erc20_test = async (options) => {
    const {
      tokenId,
      sellAmount,
      sellingPrice,
      buyAmount,
      account_a,
      account_b,
      account_c,
      account_d,
      royalty,
      commission,
    } = options

    let { exchange, registry, statici, atomicizer } = await deploy_core_contracts()
    let [erc20, erc1155] = await deploy([TestERC20, TestERC1155])

    let totalMintAmount = buyAmount * sellingPrice
    await erc20.mint(account_b, totalMintAmount)
    await erc1155.mint(account_a, tokenId, sellAmount)

    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')

    await registry.registerProxy({ from: account_b })
    let proxy2 = await registry.proxies(account_b)
    assert.equal(true, proxy2.length > 0, 'no proxy address for account b')

    await erc1155.setApprovalForAll(proxy1, true, { from: account_a })

    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)

    let tradingAmount = buyAmount * sellingPrice
    let commissionAmount = commission * tradingAmount
    let royaltyAmount = royalty * (tradingAmount - commissionAmount)
    let finalAmount = tradingAmount - commissionAmount - royaltyAmount

    console.log("tradingAmount:     %d", tradingAmount)
    console.log("commissionAmount:  %d", commissionAmount)
    console.log("royaltyAmount:     %d", royaltyAmount)
    console.log("finalAmount:       %d", finalAmount)

    const erc1155c = new web3.eth.Contract(erc1155.abi, erc1155.address)
    const erc20c = new web3.eth.Contract(erc20.abi, erc20.address)
    const coin = null;
    const selectorOne = web3.eth.abi.encodeFunctionSignature('anyAddOne(bytes,address[7],uint8[2],uint256[6],bytes,bytes)')
    const selectorTwo = web3.eth.abi.encodeFunctionSignature('anyAddOne(bytes,address[7],uint8[2],uint256[6],bytes,bytes)')

    const params1 = web3.eth.abi.encodeParameters(
      ['address[2]', 'uint256[3]'],
      [[erc1155.address, atomicizer.address], [tokenId, buyAmount, sellingPrice]]
    )
    const params2 = web3.eth.abi.encodeParameters(
      ['address[2]', 'uint256[3]'],
      [[atomicizer.address, erc1155.address], [tokenId, sellingPrice, buyAmount]]
    )

    const one = {
      registry: registry.address,
      maker: account_a,
      staticTarget: statici.address,
      staticSelector: selectorOne,
      staticExtradata: params1,
      maximumFill: sellAmount,
      listingTime: '0',
      expirationTime: '10000000000',
      salt: '11'
    }
    const two = {
      registry: registry.address,
      maker: account_b,
      staticTarget: statici.address,
      staticSelector: selectorTwo,
      staticExtradata: params2,
      maximumFill: sellingPrice * buyAmount,
      listingTime: '0',
      expirationTime: '10000000000',
      salt: '12'
    }

    const firstData = erc1155c.methods.safeTransferFrom(
      account_a,
      account_b,
      tokenId,
      buyAmount,
      "0x"
    ).encodeABI()

    let params = []
    if (finalAmount !== 0) {
      params.push(await getTransferData(account_b, account_a, finalAmount, coin));
    }

    if (commissionAmount !== 0) {
      params.push(await getTransferData(account_b, account_c, commissionAmount, coin));
    }

    if (royaltyAmount !== 0) {
      params.push(await getTransferData(account_b, account_d, royaltyAmount, coin));
    }

    let addrs = [], values = [], calldataLengths = [];
    let calldatas = ''
    for (let i = 0; i < params.length; i++) {
      const data = params[i]

      addrs.push(data.addr)
      values.push(data.value)
      calldataLengths.push(data.len)
      calldatas += data.data.slice(2)
    }

    const secondData = atomicizerc.methods.atomicize(addrs, values, calldataLengths, '0x' + calldatas).encodeABI()

    const firstCall = { target: erc1155.address, howToCall: 0, data: firstData }
    const secondCall = { target: atomicizer.address, howToCall: 1, data: secondData }

    let sigOne = await exchange.sign(one, account_a)
    let sigTwo = await exchange.sign(two, account_b)

    await exchange.atomicMatchWith(
      one,
      sigOne,
      firstCall,
      two,
      sigTwo,
      secondCall,
      ZERO_BYTES32,
      { from: account_b }
    )

    let [
      account_a_erc1155_balance,
      account_a_erc20_balance,
      account_b_erc20_balance,
      account_c_erc20_balance,
      account_d_erc20_balance,
      account_b_erc1155_balance
    ] = await Promise.all([
      erc1155.balanceOf(account_a, tokenId),
      erc20.balanceOf(account_a),
      erc20.balanceOf(account_b),
      erc20.balanceOf(account_c),
      erc20.balanceOf(account_d),
      erc1155.balanceOf(account_b, tokenId)
    ])

    console.log("account_a balance: %d, erc1155: %s", account_a_erc20_balance.toNumber(), account_a_erc1155_balance.toNumber())
    console.log("account_b balance: %d, erc1155: %s", account_b_erc20_balance.toNumber(), account_b_erc1155_balance.toNumber())
    console.log("account_c balance: %d", account_c_erc20_balance.toNumber())
    console.log("account_d balance: %d", account_d_erc20_balance.toNumber())

    assert.equal(account_a_erc20_balance.toNumber(), finalAmount, 'Incorrect ERC20 balance from account A')
    assert.equal(account_b_erc20_balance.toNumber(), 0, 'Incorrect ERC20 balance from account B')
    assert.equal(account_c_erc20_balance.toNumber(), commissionAmount, 'Incorrect ERC20 balance from account C')
    assert.equal(account_d_erc20_balance.toNumber(), royaltyAmount, 'Incorrect ERC20 balance from account D')
    assert.equal(account_b_erc1155_balance.toNumber(), buyAmount, 'Incorrect ERC1155 balance from account B')
    assert.equal(account_a_erc1155_balance.toNumber(), sellAmount - buyAmount, 'Incorrect ERC1155 balance from account B')
  }

  it('StaticMarket: matches erc1155 <> erc20 order, 1 fill', async () => {
    const price = 10000
    const mintAmount = 2

    return any_erc1155_for_erc20_test({
      tokenId: 1, 
      sellAmount: mintAmount,
      sellingPrice: price,
      buyAmount: 1,
      account_a: accounts[0],
      account_b: accounts[1],
      account_c: accounts[2],
      account_d: accounts[3],
      royalty: 0.2,
      commission: 0.025,
    })
  })
})
m3tablock commented 2 years ago

@smitea hop into the Wyvern Discord: https://discord.com/invite/weUTpah286