Skip to content

Latest commit

 

History

History
627 lines (546 loc) · 23.1 KB

ethersignal.md

File metadata and controls

627 lines (546 loc) · 23.1 KB

EtherSignal Analysis

Following up on our introductory guide to the EVM, we now analyze a real contract.

The contract, ethersignal.sol, is as follows:

contract EtherSignal {
        event LogSignal(bytes32 indexed positionHash, bool pro, address addr);
        function setSignal(bytes32 positionHash, bool pro) {
                for (uint i = 0; i < 4096; i++) { } // burn some gas to increase DOS cost
                LogSignal(positionHash, pro, msg.sender);
                if(!msg.sender.send(this.balance)){ throw; }
    }

    function () {
        throw;
    }
}

It is obviously a very simple contract, whose design goal is simply to burn some gas, log an event, and NEVER hold any ether. Let us step through the byte code to see how it works.

After compiling with solc --optimize --bin-runtime -o . ethersignal.sol we get the following code:

$ echo $(cat EtherSignal.bin-runtime) | disasm 
60606040523615601d5760e060020a60003504637a6668bf81146023575b603f6002565b603f60043560243560005b6110008110156046575b600101602e565b005b505050565b606082815273ffffffffffffffffffffffffffffffffffffffff331660805283907fdfe72b85658ece2ea9a0485273e99806605f396deff71c0650ea0e4bb691ca8b90604090a273ffffffffffffffffffffffffffffffffffffffff33811690600090301631606082818181858883f193505050501515604157600256
0      PUSH1  => 60
2      PUSH1  => 40
4      MSTORE
5      CALLDATASIZE
6      ISZERO
7      PUSH1  => 1d
9      JUMPI
10     PUSH1  => e0
12     PUSH1  => 02
14     EXP
15     PUSH1  => 00
17     CALLDATALOAD
18     DIV
19     PUSH4  => 7a6668bf
24     DUP2
25     EQ
26     PUSH1  => 23
28     JUMPI
29     JUMPDEST
30     PUSH1  => 3f
32     PUSH1  => 02
34     JUMP
35     JUMPDEST
36     PUSH1  => 3f
38     PUSH1  => 04
40     CALLDATALOAD
41     PUSH1  => 24
43     CALLDATALOAD
44     PUSH1  => 00
46     JUMPDEST
47     PUSH2  => 1000
50     DUP2
51     LT
52     ISZERO
53     PUSH1  => 46
55     JUMPI
56     JUMPDEST
57     PUSH1  => 01
59     ADD
60     PUSH1  => 2e
62     JUMP
63     JUMPDEST
64     STOP
65     JUMPDEST
66     POP
67     POP
68     POP
69     JUMP
70     JUMPDEST
71     PUSH1  => 60
73     DUP3
74     DUP2
75     MSTORE
76     PUSH20  => ffffffffffffffffffffffffffffffffffffffff
97     CALLER
98     AND
99     PUSH1  => 80
101    MSTORE
102    DUP4
103    SWAP1
104    PUSH32  => dfe72b85658ece2ea9a0485273e99806605f396deff71c0650ea0e4bb691ca8b
137    SWAP1
138    PUSH1  => 40
140    SWAP1
141    LOG2
142    PUSH20  => ffffffffffffffffffffffffffffffffffffffff
163    CALLER
164    DUP2
165    AND
166    SWAP1
167    PUSH1  => 00
169    SWAP1
170    ADDRESS
171    AND
172    BALANCE
173    PUSH1  => 60
175    DUP3
176    DUP2
177    DUP2
178    DUP2
179    DUP6
180    DUP9
181    DUP4
182    CALL
183    SWAP4
184    POP
185    POP
186    POP
187    POP
188    ISZERO
189    ISZERO
190    PUSH1  => 41
192    JUMPI
193    PUSH1  => 02
195    JUMP

Let us go through it in chunks. First we have

0      PUSH1  => 60
2      PUSH1  => 40
4      MSTORE

which seems to be pointless, as the memory is later overwritten, though the program counter location 0x02 is used later as an invalid jumpdest which serves as a generic yet identifiable means for solidity to throw exceptions and revert all state transitions. Next we load the size of the call-data and check if it is empty. If so, the caller has not correctly specified the function to call, and so the execution should throw.

5      CALLDATASIZE
6      ISZERO
7      PUSH1  => 1d
9      JUMPI

We see here that, if ISZERO returns a 0x01, we jump to PC 0x1d (29):

29     JUMPDEST
30     PUSH1  => 3f
32     PUSH1  => 02
34     JUMP

PC 29 is a valid jump destination, but we then jump to PC 2, which is an invalid jump destination, and causes execution to halt.

We can confirm this behaviour with an execution:

$ evm --code $(cat EtherSignal.bin-runtime) --debug
VM STAT 12 OPs
PC 00000000: PUSH1 GAS: 9999999997 COST: 3
STACK = 0
MEM = 0
STORAGE = 0

PC 00000002: PUSH1 GAS: 9999999994 COST: 3
STACK = 1
0000: 0000000000000000000000000000000000000000000000000000000000000060
MEM = 0
STORAGE = 0

PC 00000004: MSTORE GAS: 9999999982 COST: 12
STACK = 2
0000: 0000000000000000000000000000000000000000000000000000000000000040
0001: 0000000000000000000000000000000000000000000000000000000000000060
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
STORAGE = 0

PC 00000005: CALLDATASIZE GAS: 9999999980 COST: 2
STACK = 0
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000006: ISZERO GAS: 9999999977 COST: 3
STACK = 1
0000: 0000000000000000000000000000000000000000000000000000000000000000
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000007: PUSH1 GAS: 9999999974 COST: 3
STACK = 1
0000: 0000000000000000000000000000000000000000000000000000000000000001
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000009: JUMPI GAS: 9999999964 COST: 10
STACK = 2
0000: 000000000000000000000000000000000000000000000000000000000000001d
0001: 0000000000000000000000000000000000000000000000000000000000000001
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000029: JUMPDEST GAS: 9999999963 COST: 1
STACK = 0
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000030: PUSH1 GAS: 9999999960 COST: 3
STACK = 0
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000032: PUSH1 GAS: 9999999957 COST: 3
STACK = 1
0000: 000000000000000000000000000000000000000000000000000000000000003f
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000034: JUMP GAS: 9999999949 COST: 8
STACK = 2
0000: 0000000000000000000000000000000000000000000000000000000000000002
0001: 000000000000000000000000000000000000000000000000000000000000003f
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

PC 00000034: JUMP GAS: 9999999949 COST: 8 ERROR: invalid jump destination (PUSH1) 2
STACK = 1
0000: 000000000000000000000000000000000000000000000000000000000000003f
MEM = 96
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0016: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0048: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0064: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60  ...............`
STORAGE = 0

OUT: 0x error: invalid jump destination (PUSH1) 2

If the call-data size is not zero, we load the first four bytes and check if they match the correct function identifier

10     PUSH1  => e0
12     PUSH1  => 02
14     EXP
15     PUSH1  => 00
17     CALLDATALOAD
18     DIV
19     PUSH4  => 7a6668bf
24     DUP2
25     EQ
26     PUSH1  => 23
28     JUMPI

Here, we divide the 32-bytes of the call-data from position 0x0 by 2^224 to get the four-byte function identifier and compare it with the function identifier for the setSignal function, 7a6668bf. We can confirm what the function identifier should be in python:

>>> import sha3
>>> sha3.sha3_256("setSignal(bytes32,bool)").hexdigest()[:8]
'7a6668bf'

If the function identifier matches, we jump to PC 0x23 (35), otherwise we continue to PC 0x1d (29), which we have already seen results in an invalid jump destination exception.

So, if the call-data is empty, or if the first four bytes are not equal to the function identifier, the execution throws an exception and reverts all state. We can similarly confirm this by passing any arbitrary call-data that does not begin with 7a6668bf, eg. $ evm --code $(cat EtherSignal.bin-runtime) --debug --input deadbeef

If the first four bytes of the call-data are 7a6668bf, then the function is being correctly called and the EQ returns 0x01, so we jump to PC 0x23 (35):

35     JUMPDEST
36     PUSH1  => 3f
38     PUSH1  => 04
40     CALLDATALOAD
41     PUSH1  => 24
43     CALLDATALOAD
44     PUSH1  => 00

It is a valid jump destination. We then load the arguments from the call-data (positions 0x04 and 0x24) so they are available on the stack for later, and push a 0x00, which is the initial value of the counter for the loop. Note we have also pushed a 0x3f to the stack - this will be used in a jump at the end, but pretty much just sits at the bottom of the stack until then. Next up is the JUMPDEST which marks the top of the loop:

46     JUMPDEST
47     PUSH2  => 1000
50     DUP2
51     LT
52     ISZERO
53     PUSH1  => 46
55     JUMPI
56     JUMPDEST
57     PUSH1  => 01
59     ADD
60     PUSH1  => 2e
62     JUMP
63     JUMPDEST
64     STOP
65     JUMPDEST
66     POP
67     POP
68     POP
69     JUMP
70     JUMPDEST

The loop is expected to run 4096 (0x1000) times, so we push that to the stack, and duplicate the second item on the stack (the counter), so we can check if it is less than 0x1000. If not, the result of LT will be 0x0, so ISZERO will push 0x01, and we will jump to PC 0x46 (70), thus exiting the loop. Otherwise, we add 0x01 to the counter on the stack and jump to PC 0x2e (46), ie. back to the top of the loop.

Note that every time through, when we are at the JUMPDEST at the top of the loop, the stack should look like:

PC 00000046: JUMPDEST GAS: 9999999885 COST: 1
STACK = 5
0000: 0000000000000000000000000000000000000000000000000000000000000000
0001: 0000000000000000000000000000000000000000000000000000000000000000
0002: 0000000000000000000000000000000000000000000000000000000000000000
0003: 000000000000000000000000000000000000000000000000000000000000003f
0004: 000000000000000000000000000000000000000000000000000000007a6668bf

where the top element is the counter (increments each pass through the loop), and the next two elements are the two arguments passed in the call-data (bool pro and bytes32 positionHash, respectively, here we are showing an example where the arguments were simply empty)

The loop progresses in this way, doing nothing of interest but burning gas, until the counter value reaches 0x1000, at which point we jump to PC 0x46 (70), which is a valid jump destination, and continue by logging an event as follows:

70     JUMPDEST
71     PUSH1  => 60
73     DUP3
74     DUP2
75     MSTORE
76     PUSH20  => ffffffffffffffffffffffffffffffffffffffff
97     CALLER
98     AND
99     PUSH1  => 80
101    MSTORE
102    DUP4
103    SWAP1
104    PUSH32  => dfe72b85658ece2ea9a0485273e99806605f396deff71c0650ea0e4bb691ca8b
137    SWAP1
138    PUSH1  => 40
140    SWAP1
141    LOG2

Note when the loop ends, we should still have the counter (now at 0x1000) and the two call-data arguments on the stack. So here, we push 0x60, copy the second call-data argument to the top, copy the 0x60 to the top of the stack, and store the call-data argument in memory at position 0x60. Now we push 20-bytes of 0xff and the caller address, and perform boolean AND, ensuring the resulting item on the stack has at most 20 non-zero bytes - though the result of CALLER should always be 20-bytes anyways, so not sure this is necessary. We now store the resulting address at position 0x80 in memory. A few more stack operations get everything prepared for the LOG2. For instance, right before the LOG2 the stack looks something like:

0000: 0000000000000000000000000000000000000000000000000000000000000060 - position in memory
0001: 0000000000000000000000000000000000000000000000000000000000000040 - size in memory
0002: dfe72b85658ece2ea9a0485273e99806605f396deff71c0650ea0e4bb691ca8b - topic 1 (hard coded by solidity)
0003: 0000000000000000000000000000000000000000000000000000000000000000 - topic 2 (`bytes32 positionHash` argument)
0004: 0000000000000000000000000000000000000000000000000000000000001000 - counter variable from the loop
0005: 0000000000000000000000000000000000000000000000000000000000000000 - call data (`bool pro`)
0006: 0000000000000000000000000000000000000000000000000000000000000000 - call data (`bytes32 positionHash`)
0007: 000000000000000000000000000000000000000000000000000000000000003f 
0008: 000000000000000000000000000000000000000000000000000000007a6668bf

Here we are using LOG2, which takes a starting location and size for the byte-array to log from memory, as well as two additional items from the stack which serve as topics. In this case, starting from position 0x60 in memory we grab 0x40 (64) bytes, which includes the bool pro argument and the caller address we just stored in memory. In addition we pass two topics, one being the bytes32 positionHash argument, the other being a hardcoded hash provided by solidity.

Next up, we send any balance the contract has to the caller with a CALL:

142    PUSH20  => ffffffffffffffffffffffffffffffffffffffff
163    CALLER
164    DUP2
165    AND
166    SWAP1
167    PUSH1  => 00
169    SWAP1
170    ADDRESS
171    AND
172    BALANCE
173    PUSH1  => 60
175    DUP3
176    DUP2
177    DUP2
178    DUP2
179    DUP6
180    DUP9
181    DUP4
182    CALL

This is mostly just a mess of stack manipulation to arrange the arguments for the call. Here is what the stack looks like just before the call, with elements annotated:

0000: 0000000000000000000000000000000000000000000000000000000000000000 - gas
0001: 000000000000000000000000000000000000000000000000000073656e646572 - address
0002: 0000000000000000000000000000000000000000000000000000000000000000 - value
0003: 0000000000000000000000000000000000000000000000000000000000000060 - inputOffset
0004: 0000000000000000000000000000000000000000000000000000000000000000 - inputSize
0005: 0000000000000000000000000000000000000000000000000000000000000060 - returnOffset
0006: 0000000000000000000000000000000000000000000000000000000000000000 - returnSize
0007: 0000000000000000000000000000000000000000000000000000000000000060
0008: 0000000000000000000000000000000000000000000000000000000000000000
0009: 0000000000000000000000000000000000000000000000000000000000000000
0010: 000000000000000000000000000000000000000000000000000073656e646572
0011: 0000000000000000000000000000000000000000000000000000000000001000
0012: 0000000000000000000000000000000000000000000000000000000000000000
0013: 0000000000000000000000000000000000000000000000000000000000000000
0014: 000000000000000000000000000000000000000000000000000000000000003f
0015: 000000000000000000000000000000000000000000000000000000007a6668bf

A call takes arguments of the stack in the following order: gas, address, value, inputOffset, inputSize, returnOffset, returnSize; where offset and size parameters refer to the memory array. In this case, we pay 0x0 gas to send to the caller (address 000000000000000000000000000073656e646572) 0x0 wei. The input is copied from position 0x60, length 0x0, and the return value is stored at position 0x60, length 0x0. In essence, this is a dummy call, since it provides no gas and no data. Note that in the solidity contract, the send function was used, which is exactly a high-level alias for this kind of dummy call:

msg.sender.send(this.balance)

The intention is to send any balance the contract may have received back to the sender. We have zero desire for any code to execute, so we provide zero gas. The default gas of the CALL opcode will be deducted, and that is all that must be paid to perform the necessary value transfer.

If the CALL succeeds, it pushes 0x01 to the stack and writes the return to memory. Otherwise, it pushes 0x0 to the stack.

Note that, if the caller is actually a contract, then the CALL will attempt to execute the code of that contract, but since it is provided no gas, it will throw an OutOfGas exception, causing the CALL to fail, and return 0x0 to the stack. This would leave our contract with whatever balance it was sent, so we check the value pushed to the stack and throw an exception if it is zero. This is why the send function is wrapped in:

if(!msg.sender.send(this.balance)){ throw; }

To see this in byte code, we have to wade through some more stack manipulation. First, not that a successful completion of a call would give us a stack like:

0000: 0000000000000000000000000000000000000000000000000000000000000001
0001: 0000000000000000000000000000000000000000000000000000000000000060
0002: 0000000000000000000000000000000000000000000000000000000000000000
0003: 0000000000000000000000000000000000000000000000000000000000000000
0004: 000000000000000000000000000000000000000000000000000073656e646572
0005: 0000000000000000000000000000000000000000000000000000000000001000
0006: 0000000000000000000000000000000000000000000000000000000000000000
0007: 0000000000000000000000000000000000000000000000000000000000000000
0008: 000000000000000000000000000000000000000000000000000000000000003f
0009: 000000000000000000000000000000000000000000000000000000007a6668bf

(An unsuccessful call would have a 0x0 at the top instead of the 0x1).

Before checking the return value, we remove three unneeded elements from the stack by swaping out the return value below them and popping them off:

183    SWAP4
184    POP
185    POP
186    POP
187    POP

Note that SWAPN followed by POP N times leaves whatever what was on top of the stack before the SWAPN still at the top (in this case, the return value of the call). Now we can check if it is zero:

188    ISZERO
189    ISZERO
190    PUSH1  => 41
192    JUMPI
193    PUSH1  => 02
195    JUMP

For some reason, the ISZERO is run twice, which leaves the value unchanged (assuming it was a 0x0 or 0x1 to begin with). Ie. (ISZERO (ISZERO 0x0)) = 0x0 and (ISZERO (ISZERO 0x1)) = 0x1. In any case, if the call returned a 0x1, we jump to PC 0x41 (65). Otherwise, we throw an exception by jumping to the invalid destination at PC 0x2 (2).

At PC 65, a valid jump destination, we pop a few more un-needed items from the stack:

65     JUMPDEST
66     POP
67     POP
68     POP

leaving us with:

0001: 000000000000000000000000000000000000000000000000000000000000003f
0002: 000000000000000000000000000000000000000000000000000000007a6668bf

This 0x3f (63) was pushed long ago, at PC 36, when we mentioned it would be used by a jump. Here is that jump:

69     JUMP

It takes us to PC 63:

63     JUMPDEST
64     STOP

And that is that! Contract execution has completed successfully.

Remarks

EtherSignal is a simple contract intended to allow users to signal whether they are for or against some proposal, and to provide a hash of their position statement. The contract wastes some amount of gas in a useless for-loop, before logging an event containing the caller's address, vote, and position statement hash. In order to avoid ever holding any ether, the contract always either throws an exception (via an invalid JUMPDEST), or sends its balance back to the caller. If the send fails, it will also throw an exception. Since throwing an exception or running out of gas reverts all state associated with the transaction (except the payment of gas), this contract is guaranteed to never hold a balance at the end of an execution.

Of course, it goes without saying that the contract will hold a balance if some value is sent in the transaction which creates it, but this is entirely in the control of the user deploying the contract to begin with. Worst case, this balance will be sent to the first successful user of the contract.

Note that the use of a dumb for-loop to burn gas may have unintended consequences. In particular, since it is guaranteed to use a fixed amount of gas and make no change to the state, clever miners can attempt to DoS other nodes by calling the contract many times in blocks they mine (hence paying gas to themselves) and simply not running the loop, since they know it has no effect. Other, less clever users and miners will be forced to run the loop, thus providing a potentially strong advantage to the clever miner. Fortunately, this loop is easily identifiable, and the Nash equilibrium is such that all users will eventually just not run it - though we await the deployment of tools that would permit such intelligent behaviour.