Skip to content

Commit aec9914

Browse files
authored
Merge pull request #172 from input-output-hk/feature/fix-suicide-refund
EC-170: Fix gas refund for self destruct when using CALLs
2 parents a8a5087 + 057f802 commit aec9914

File tree

12 files changed

+70
-22
lines changed

12 files changed

+70
-22
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
pragma solidity ^0.4.10;
2+
3+
contract CallSelfDestruct {
4+
5+
function callDestruct() {
6+
CallSelfDestruct firstCall = CallSelfDestruct(this);
7+
firstCall.doSelfdestruct();
8+
9+
CallSelfDestruct secondCall = CallSelfDestruct(this);
10+
secondCall.doSelfdestruct();
11+
}
12+
13+
function doSelfdestruct() {
14+
selfdestruct(msg.sender);
15+
}
16+
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.iohk.ethereum.vm
2+
3+
import io.iohk.ethereum.vm.utils.EvmTestEnv
4+
import org.scalatest.{FreeSpec, Matchers}
5+
6+
class CallSelfDestructSpec extends FreeSpec with Matchers {
7+
8+
"EVM running CallSelfDestruct contract" - {
9+
10+
"should refund only once" in new EvmTestEnv {
11+
val (_, callSelfDestruct) = deployContract("CallSelfDestruct")
12+
13+
val callRes = callSelfDestruct.callDestruct().call()
14+
15+
callRes.error shouldBe None
16+
callRes.gasRefund shouldBe 24000
17+
}
18+
}
19+
20+
}

src/main/scala/io/iohk/ethereum/ledger/Ledger.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ class LedgerImpl(vm: VM, blockchainConfig: BlockchainConfig) extends Ledger with
127127
val resultWithErrorHandling: PR =
128128
if(result.error.isDefined) {
129129
//Rollback to the world before transfer was done if an error happened
130-
result.copy(world = checkpointWorldState, addressesToDelete = Nil, logs = Nil)
130+
result.copy(world = checkpointWorldState, addressesToDelete = Set.empty, logs = Nil)
131131
} else
132132
result
133133

@@ -314,7 +314,7 @@ class LedgerImpl(vm: VM, blockchainConfig: BlockchainConfig) extends Ledger with
314314
* @param worldStateProxy
315315
* @return a worldState equal worldStateProxy except that the accounts from addressesToDelete are deleted
316316
*/
317-
private[ledger] def deleteAccounts(addressesToDelete: Seq[Address])(worldStateProxy: InMemoryWorldStateProxy): InMemoryWorldStateProxy =
317+
private[ledger] def deleteAccounts(addressesToDelete: Set[Address])(worldStateProxy: InMemoryWorldStateProxy): InMemoryWorldStateProxy =
318318
addressesToDelete.foldLeft(worldStateProxy){ case (world, address) => world.deleteAccount(address) }
319319

320320
}

src/main/scala/io/iohk/ethereum/vm/OpCode.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ case object CREATE extends OpCode(0xf0, 3, 1, _.G_create) {
696696
val availableGas = state.gas - (constGasFn(state.config.feeSchedule) + varGas(state))
697697
val startGas = state.config.gasCap(availableGas)
698698

699-
val context = ProgramContext[W, S](newEnv, newAddress, startGas, world2, state.config)
699+
val context = ProgramContext[W, S](newEnv, newAddress, startGas, world2, state.config, state.addressesToDelete)
700700
val result = VM.run(context)
701701

702702
val codeDepositGas = state.config.calcCodeDepositCost(result.returnData)
@@ -780,7 +780,8 @@ sealed abstract class CallOp(code: Int, delta: Int, alpha: Int) extends OpCode(c
780780
env = env,
781781
receivingAddr = toAddr,
782782
startGas = startGas,
783-
world = world1)
783+
world = world1,
784+
initialAddressesToDelete = state.addressesToDelete)
784785

785786
VM.run(context)
786787
}

src/main/scala/io/iohk/ethereum/vm/PrecompiledContracts.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ object PrecompiledContracts {
4848
result,
4949
gasRemaining,
5050
context.world,
51-
Nil,
51+
Set.empty,
5252
Nil,
5353
0,
5454
error

src/main/scala/io/iohk/ethereum/vm/ProgramContext.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ object ProgramContext {
4141
* @param startGas initial gas for the execution
4242
* @param world provides interactions with world state
4343
* @param config evm config
44+
* @param initialAddressesToDelete contains initial set of addresses to delete (from lower depth calls)
4445
*/
4546
case class ProgramContext[W <: WorldStateProxy[W, S], S <: Storage[S]](
4647
env: ExecEnv,
4748
receivingAddr: Address,
4849
startGas: BigInt,
4950
world: W,
50-
config: EvmConfig)
51+
config: EvmConfig,
52+
initialAddressesToDelete: Set[Address] = Set.empty)

src/main/scala/io/iohk/ethereum/vm/ProgramResult.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ case class ProgramResult[W <: WorldStateProxy[W, S], S <: Storage[S]](
1616
returnData: ByteString,
1717
gasRemaining: BigInt,
1818
world: W,
19-
addressesToDelete: Seq[Address],
19+
addressesToDelete: Set[Address],
2020
logs: Seq[TxLogEntry],
2121
gasRefund: BigInt,
2222
error: Option[ProgramError])

src/main/scala/io/iohk/ethereum/vm/ProgramState.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ object ProgramState {
88
ProgramState(
99
context = context,
1010
gas = context.startGas,
11-
world = context.world)
11+
world = context.world,
12+
addressesToDelete = context.initialAddressesToDelete)
1213
}
1314

1415
/**
@@ -34,7 +35,7 @@ case class ProgramState[W <: WorldStateProxy[W, S], S <: Storage[S]](
3435
pc: Int = 0,
3536
returnData: ByteString = ByteString.empty,
3637
gasRefund: BigInt = 0,
37-
addressesToDelete: Seq[Address] = Nil,
38+
addressesToDelete: Set[Address] = Set.empty,
3839
logs: Vector[TxLogEntry] = Vector.empty,
3940
halted: Boolean = false,
4041
error: Option[ProgramError] = None
@@ -87,9 +88,9 @@ case class ProgramState[W <: WorldStateProxy[W, S], S <: Storage[S]](
8788
copy(returnData = data)
8889

8990
def withAddressToDelete(addr: Address): ProgramState[W, S] =
90-
copy(addressesToDelete = addressesToDelete :+ addr)
91+
copy(addressesToDelete = addressesToDelete + addr)
9192

92-
def withAddressesToDelete(addresses: Seq[Address]): ProgramState[W, S] =
93+
def withAddressesToDelete(addresses: Set[Address]): ProgramState[W, S] =
9394
copy(addressesToDelete = addressesToDelete ++ addresses)
9495

9596
def withLog(log: TxLogEntry): ProgramState[W, S] =

src/test/scala/io/iohk/ethereum/Mocks.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ object Mocks {
2727
returnData = ByteString.empty,
2828
gasRemaining = 1000000 - 25000,
2929
world = context.world,
30-
addressesToDelete = Nil,
30+
addressesToDelete = Set.empty,
3131
logs = Nil,
3232
gasRefund = 20000,
3333
error = None

src/test/scala/io/iohk/ethereum/ledger/DeleteAccountsSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class DeleteAccountsSpec extends FlatSpec with Matchers {
1313
val ledger = new LedgerImpl(new Mocks.MockVM(), blockchainConfig)
1414

1515
it should "delete no accounts when none of them should be deleted" in new TestSetup {
16-
val newWorld = InMemoryWorldStateProxy.persistState(ledger.deleteAccounts(Nil)(worldState))
16+
val newWorld = InMemoryWorldStateProxy.persistState(ledger.deleteAccounts(Set.empty)(worldState))
1717
accountAddresses.foreach{ a => assert(newWorld.getAccount(a).isDefined) }
1818
newWorld.stateRootHash shouldBe worldState.stateRootHash
1919
}
@@ -47,7 +47,7 @@ class DeleteAccountsSpec extends FlatSpec with Matchers {
4747
val validAccountAddress2 = Address(0xcdcdcd)
4848
val validAccountAddress3 = Address(0xefefef)
4949

50-
val accountAddresses = Seq(validAccountAddress, validAccountAddress2, validAccountAddress3)
50+
val accountAddresses = Set(validAccountAddress, validAccountAddress2, validAccountAddress3)
5151

5252
val worldStateWithoutPersist: InMemoryWorldStateProxy = InMemoryWorldStateProxy(
5353
storagesInstance.storages,

src/test/scala/io/iohk/ethereum/ledger/LedgerSpec.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class LedgerSpec extends FlatSpec with PropertyChecks with Matchers {
3535
error: Option[ProgramError] = None,
3636
returnData: ByteString = bEmpty,
3737
logs: Seq[TxLogEntry] = Nil,
38-
addressesToDelete: Seq[Address] = Nil): PR =
38+
addressesToDelete: Set[Address] = Set.empty): PR =
3939
ProgramResult(
4040
returnData = returnData,
4141
gasRemaining = gasLimit - gasUsed,
@@ -164,11 +164,11 @@ class LedgerSpec extends FlatSpec with PropertyChecks with Matchers {
164164

165165
it should "correctly run executeBlockTransactions for a block with one tx (that produces no errors)" in new BlockchainSetup {
166166

167-
val table = Table[BigInt, Seq[TxLogEntry], Seq[Address], Boolean](
167+
val table = Table[BigInt, Seq[TxLogEntry], Set[Address], Boolean](
168168
("gasLimit/gasUsed", "logs", "addressesToDelete", "txValidAccordingToValidators"),
169-
(defaultGasLimit, Nil, Nil, true),
169+
(defaultGasLimit, Nil, Set.empty, true),
170170
(defaultGasLimit / 2, Nil, defaultAddressesToDelete, true),
171-
(2 * defaultGasLimit, defaultLogs, Nil, true),
171+
(2 * defaultGasLimit, defaultLogs, Set.empty, true),
172172
(defaultGasLimit, defaultLogs, defaultAddressesToDelete, true),
173173
(defaultGasLimit, defaultLogs, defaultAddressesToDelete, false)
174174
)
@@ -711,7 +711,7 @@ class LedgerSpec extends FlatSpec with PropertyChecks with Matchers {
711711

712712
val initialOriginNonce = defaultTx.nonce
713713

714-
val defaultAddressesToDelete = Seq(Address(Hex.decode("01")), Address(Hex.decode("02")), Address(Hex.decode("03")))
714+
val defaultAddressesToDelete = Set(Address(Hex.decode("01")), Address(Hex.decode("02")), Address(Hex.decode("03")))
715715
val defaultLogs = Seq(defaultLog.copy(loggerAddress = defaultAddressesToDelete.head))
716716
val defaultGasPrice: UInt256 = 10
717717
val defaultGasLimit: UInt256 = 1000000

src/test/scala/io/iohk/ethereum/vm/CallOpcodesSpec.scala

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,20 @@ class CallOpcodesSpec extends WordSpec with Matchers with PropertyChecks {
316316

317317
"calling a program that executes a SELFDESTRUCT" should {
318318

319-
val context: PC = fxt.context.copy(world = fxt.worldWithSelfDestructProgram)
320-
val call = CallResult(op = CALL, context)
321-
322319
"refund the correct amount of gas" in {
320+
val context: PC = fxt.context.copy(world = fxt.worldWithSelfDestructProgram)
321+
val call = CallResult(op = CALL, context)
323322
call.stateOut.gasRefund shouldBe call.stateOut.config.feeSchedule.R_selfdestruct
324323
}
325324

325+
"not refund gas if account was already self destructed" in {
326+
val context: PC = fxt.context.copy(
327+
world = fxt.worldWithSelfDestructProgram,
328+
initialAddressesToDelete = Set(fxt.extAddr))
329+
val call = CallResult(op = CALL, context)
330+
call.stateOut.gasRefund shouldBe 0
331+
}
332+
326333
}
327334

328335
"calling a program that executes a SSTORE that clears the storage" should {

0 commit comments

Comments
 (0)