EVMPARTY: Counterparty assets in smart contracts

So one of the extra flavors we want to add to the EVM for CP is being able to send/receive CP assets.

There’s 3 possible aproaches to take and I’d like to discuss them and get ideas from other devs on this subject.

as an example contract we’ll take a crowdfund that is VERY stripped down and doesn’t adhere to any best practices, if the goal is reached the person who did the last contribution receives all of it xD

XCP

contract crowdfund {
    uint contributed = 0;  // tracks status
    uint goal = 1000;  // goal for payout
    
    function donate() {
        contributed += msg.value;  // increase contributed
         
        // check if we reached the goal
        if (contributed >= goal) {
            msg.sender.send(contributed);  // payout to the last `sender, lucky bastard
        }
    }
}

the important part here is msg.value which is the amount of XCP send (excluding gas, those 2 are seperated).
now when compiled msg.value is translated into the CALLVALUE opcode, which ‘returns’ (puts it on the stack) the value.

Option 1

So option 1 is; add a msg.asset which is the asset name of what was send and we keep using msg.value for the value,
so if this contract would be about a crowdfund using GOLDTOKEN it would be like this:

contract crowdfund {
    uint contributed = 0;  // tracks status
    uint goal = 1000;  // goal for payout
    
    function donate() {
        if (msg.asset != "GOLDTOKEN") {
            throw;  // throw will 'undo' anything that happened, so the sender get's his assets back
        }
    
        contributed += msg.value;  // increase contributed
        
        // check if we reached the goal
        if (contributed >= goal) {
            msg.sender.send(contributed, "GOLDTOKEN");  // payout to the last sender, lucky bastard
        }
    }
}

the important part here is msg.value now is either amount of XCP send or if msg.asset is not null it’s the amount of that asset send.
now when compiled msg.asset would be translated into the CALLASSET opcode, which ‘returns’ (puts it on the stack) the name of the asset.

I think this is by far the worst option, because every contract that is expecting XCP needs to check msg.asset == null or msg.asset == "XCP", any contract copied from Ethereum you could cheat by sending them some token and if it doesn’t have that check … oops.
I wanted to put the option in though for spelling out each option that I considered!

Option 2

So option 2 is; add a msg.asset and a msg.assetvalue which are the asset name of what was send and the amount of that asset send,
so if this contract would be about a crowdfund using GOLDTOKEN it would be like this:

contract crowdfund {
    uint contributed = 0;  // tracks status
    uint goal = 1000;  // goal for payout
    
    function donate() {
        if (msg.asset != "GOLDTOKEN") {
            throw;  // throw will 'undo' anything that happened, so the sender get's his assets back
        }
    
        contributed += msg.assetvalue;  // increase contributed
        
        // check if we reached the goal
        if (contributed >= goal) {
            msg.sender.sendasset(contributed, "GOLDTOKEN");  // payout to the last sender, lucky bastard
        }
    }
}

the important part here is msg.asset is the asset that was send, msg.assetvalue is the amount of that asset send.
now when compiled msg.asset would be translated into the CALLASSET opcode, which ‘returns’ (puts it on the stack) the name of the asset.
and msg.assetvalue would be translated into the CALLASSETVALUE opcode, which ‘returns’ (puts it on the stack) the amount of that asset send.

now this option isn’t bad, it’s explicit and it keeps proper backward compatability with Ethereum original contracts.

It does require adding 2 new opcodes, which isn’t a very big deal except that we have to pick a 1 byte number for those opcode,
the EVM already has ~130 opcodes and anything below 160 has been exhausted more or less because they sometimes skip a few numbers (the only reason being for easy ‘reading’).

There’s no guarentee that a number we pick won’t eventually be taken by any new features on the EVM and if we want to stay of to date and compatible with the original EVM that will conflict …

We could take the OP_NOP aproach of bitcoin and use a OP_PUSH followed by a sequence we deem unique enough OP_PUSH9 0x43 0x4e 0x54 0x52 0x50 0x52 0x54 0x59 <OUR_OPS_HERE> (that’s the HEX of CNTRPRTY, the same prefix we use in bitcoin transactions), but that eats a lot of data.

if we’d change the number we’ve assigned to our own opcode, whenever a new opcode is added to the original EVM that conflicts, then that could result in really awkward bugs so that’s a no go.

Or we could ask the EVM devs to reserve 1 opcode for and use 2 bytes for our opcodes, so if we’d get them to reserve 0xfe for us / others who fork the EVM then our opcodes would be 0xfe 0x01, 0xfe 0x02, etc.

Option 3

The EVM has a bunch of hardcoded / precompiled contracts:

  • ‘0000000000000000000000000000000000000001’: ecrecover
  • ‘0000000000000000000000000000000000000002’: sha256
  • ‘0000000000000000000000000000000000000003’: ripemd160

eg; the solidity compiler will compile sha256(DATA) to a CALL to contract 0000000000000000000000000000000000000002 with the DATA, or ecrecover(H, V, S, R) to a CALL to 00..01 with the data being H+V+S+R.

we can easily add more to these, eg;

  • ‘434e545250525459000000000000000000000001’: sendasset # CNTRPRTY prefix in hex
  • ‘434e545250525459000000000000000000000002’: assetreceived # CNTRPRTY prefix in hex

so if this contract would be about a crowdfund using GOLDTOKEN it would be like this:

contract crowdfund {
    uint contributed = 0;  // tracks status
    uint goal = 1000;  // goal for payout
    
    function donate() {
        var (assetreceived, assetvalue) = assetreceived(); 
        if (assetreceived != "GOLDTOKEN") {
            throw;  // throw will 'undo' anything that happened, so the sender get's his assets back
        }
    
        contributed += assetvalue;  // increase contributed
        
        // check if we reached the goal
        if (contributed >= goal) {
            sendasset(msg.sender, contributed, "GOLDTOKEN");  // payout to the last sender, lucky bastard
        }
    }
}

the important part here is assetreceived that returns the asset that was send and the amount of that asset send.
now when compiled assetreceived() would be translated into the CALL 434e545250525459000000000000000000000002 which ‘returns’ (puts it on the stack) the name of the asset and the amount send.
and sendasset becomes CALL 434e545250525459000000000000000000000001 <ADDRESS> <VALUE> <ASSET>.

this option requires the least amount of change, but it also feels the least ‘native’.

Thanks for all of the detail in the post. If we can get a reserved opcode, I like option 2. OP_NOP with option 2 is another possibility, but it is kind of hackish and obtuse (albeit at a level few will see – although it will bloat the bytecode some…but probably not much overall, unless the contract has a bunch of sends in it).

Option 2 with randomly picking an op code and hoping the Eth dev team doesn’t use it is a non-starter for me. Too much risk.

What would you estimate at the complexity in maintaining an option 2 over something simpler, like an option 3?

Option 3 is the cleaner technical way, and the syntax doesn’t bother me, but yes, I agree that it isn’t very “solidity-like”…

As a developer, I don’t see any difference between Options 2 and 3 for a hands-off Ethereum-Counterparty Solidity port. If I understand it correctly, the differences in Option 3 only become pertinent to you when you want to adapt existing Solidity code to use Counterparty native assets.

From my perspective, enabling Solidity-like code to run on the Bitcoin blockchain is 99% of the value here. If I wanted to use Counterparty-native assets in my Solidity code, I could care less if I needed to learn a few additional built-ins.

I strongly favor prioritizing the core EVM infrastructure in Counterparty over “picture-perfect” compatibility with Ethereum-solidity. When it comes to safety critical software, wispful developer conveniences like that don’t make a lick of difference. I’d much rather have a bulletproof core infrastructure without any deeply ingrained technical debt.

Option 2 risks breaking compatibility with the EVM, which is by far a worse outcome than having contract developers learn a few built-ins, and then only if they want to use Counterparty-native assets. Imagine being in the position where Counterparty is much bigger and harder to hard-fork, yet EVM compatibility is broken only for the reason of developer convenience! Sounds like a completely avoidable disaster.

Doesn’t Option 3 minimize technical debt in the core of Counterparty? Is it that important that we have picture-perfect ports when 99% of the value here is bringing readable smart contract language to the Bitcoin blockchain?

1 Like