vegaprotocol / Vega_Token_V2

The Vega Protocol ERC20 token, with vesting tranches

Home Page:https://vega.xyz

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bulk issuance support contract

emilbayes opened this issue · comments

Try developing a contract to batch issue tokens instead of many independent transactions. This will give us atomicity and potentially gas savings

The issuance was successful. Below is a small writeup of how it was done.

Issuing in bulking using a specialised EVM batch contract would allow us to save on gas usage and make issuance atomic. A ethereum transaction has a minimum cost of 21,000 gas, while a CALL instruction without any transfer of eth is only 9,000 gas, hence, batching CALLs can save a lot on transaction costs. Below is the YUL contract for doing the batches and the script for assembling and sending the batches:

object "BatchContract" {
  code {
    // Copy the runtime contract code into memory
    datacopy(0, dataoffset("runtime"), datasize("runtime"))

    // setimmutable allows one to modify all occurences of a placeholder in
    // copied contract code. We do this twice for this contract, setting who
    // can call the contract and where the contract should branch off to
    setimmutable(0, "controller", caller())

    // Loading arguments to the "constructor" (ie. this code) requires loading
    // memory off the end of the expected initialisation code. Here we want to
    // load the Ethereum address of the vesting contract, which will be appended
    // to the byte code. We use `codecopy` as the EVM does not distinguish here
    // between code and data
    datacopy(datasize("runtime"), datasize("BatchContract"), 32)

    // setimmutable can only replace with values from the stack, hence why we
    // loaded the argument into memory above and then read that piece of memory
    // onto the stack (what mload is)
    setimmutable(0, "vesting_address", mload(datasize("runtime")))

    // Return the range of the actual runtime code, even though we actually
    // used more memory than that
    return(0, datasize("runtime"))
  }

  object "runtime" {
    code {
      // check for authorised sender. Note that `invalid` is used here, as it is
      // cheaper than revert(0,0), as invalid is a single instruction, while the
      // other would be `PUSH1 0x0 DUP1 REVERT`
      if iszero(eq(caller(), loadimmutable("controller"))) { invalid() }

      // Input is buffer of 20 byte address, 1 byte tranche, 10 byte amount
      // Memory layout is:
      // 4 byte function selector
      // 20 byte address right aligned to 32 bytes
      // 1 bytes uint8 aligned to 32 bytes
      // 10 byte amount right aligned to 32 byte amount

      // Method ID for issue_into_tranche(address,uint8,uint256)
      mstore(0, 0xe2de6e6d00000000000000000000000000000000000000000000000000000000)

      let len := calldatasize()
      for { let i := 0 } lt(i, len) { i := add(i, 31) }
      {
        // Load a word at a time (32 bytes). This means we will have a spare byte
        // at the end of no use
        let chunk := calldataload(i)

        // shift over the 20 byte address to word size, clearing the top bits
        // at the same time
        let user_addr := shr(96, chunk)
        mstore(4, user_addr)

        // We can load the tranche id as a single byte without having to do bit
        // hacking
        let tranche := byte(20, chunk)
        mstore8(67, tranche)

        // We have to clear out the spare byte we read off the end
        let amount := and(chunk, 0xffffffffffffffffffff00)
        // this is why this number looks odd
        mstore(69, amount)

        // We are just going to send off all the available gas
        let gas_stipend := gas()
        let vesting_address := loadimmutable("vesting_address")
        let inp := 0
        let inpsize := 100 // 4 + 32 + 32 + 32

        // We don't care about the result status or return data, so pop removes
        // the result from the stack, and the two 0s at the end leaves no memory
        // for the result to be written to
        pop(call(gas_stipend, vesting_address, 0, inp, inpsize, 0, 0))
      }

      // like invalid, stop is a cheaper way to signal that execution has ended,
      // but here without raising any issues
      stop()
    }
  }
}
const serde = require('eth-serde')
const contract = require('./build/contract.json') // Compiled YUL contract
const helpers = require('eth-helpers')
const signer = require('eth-sign')
const keygen = require('eth-keygen')

const Nanoeth = require('nanoeth/http')
const pk = Buffer.from('...', 'hex')
const chainId = 1

const eth = new Nanoeth('http://localhost:8545')

const key = keygen(pk, chainId)

const raw = require('./data.json')
const batchContractAddr = '0x...'

for (const row of raw) {
  if (/^0x[a-fA-F0-9]{40}$/.test(row.address) == false) throw new Error(JSON.stringify(row))
  if (row.tranche_id < 0 || row.tranche_id > 9) throw new Error(JSON.stringify(row))
  if (BigInt(row.amount).toString(16).length > 20)  throw new Error(JSON.stringify(row))
}

const data = raw.map(r => r.address.slice(2) + r.tranche_id.toString(16).padStart(2, '0') + BigInt(r.amount).toString(16).padStart(20, '0'))
let full = ''

;(async () => {
  var j = 1 // nonce
  for (var i = 0; i < data.length; j++) {
    let buf = data.slice(i, i + 160).join('')
    i += buf.length / 62
    while (true) {
      let next = data.slice(i, i + 15).join('')
      i += next.length / 62

      if (next === '') {
        const est = await eth.estimateGas({
          from: key.address,
          to: '0x' + batchContractAddr,
          data: '0x' + buf
        }, 'latest')

        console.log(BigInt(est), buf.length / 62)
        break
      }

      buf += next

      const est = await eth.estimateGas({
        from: key.address,
        to: '0x' + batchContractAddr,
        data: '0x' + buf
      }, 'latest')

      if (BigInt(est) >= 12500000n) {
        console.log(BigInt(est), buf.length / 62)
        break
      }
    }

    const tx = {
      nonce: Buffer.from(j.toString(16).padStart(10, '0'), 'hex'),
      gasPrice: Buffer.from('08ff33a4e8', 'hex'),
      gasLimit: Buffer.from('dd40a0', 'hex'),
      to: Buffer.from(batchContractAddr, 'hex'),
      value: Buffer.from('00', 'hex'),
      data: Buffer.from(buf, 'hex')
    }

    const stx = signer.sign(tx, pk, chainId)

    console.log(i)
    full += buf

    console.log(stx.raw.toString('hex'))

    await eth.sendRawTransaction('0x' + stx.raw.toString('hex'))
  }

  console.log(full === data.join(''))
})()