Skip to content

Instantly share code, notes, and snippets.

@bzpassersby
Created May 25, 2023 15:00
Show Gist options
  • Select an option

  • Save bzpassersby/20fed702d2498bf3c5b9a60e8da9b6f8 to your computer and use it in GitHub Desktop.

Select an option

Save bzpassersby/20fed702d2498bf3c5b9a60e8da9b6f8 to your computer and use it in GitHub Desktop.
/* eslint-disable camelcase */
import { ethers, upgrades } from 'hardhat'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { AddressZero } from '@ethersproject/constants'
import { NON_NULL_BYTES32, NON_ZERO_ADDRESS } from './utils/constants'
import { expect } from 'chai'
import { ERROR_STRINGS } from './utils/errors'
import { deployL2Test, transferOwnershipTest } from './utils/fixtures'
import { L2ECO, L2ECOBridge, ProxyAdmin } from '../typechain-types'
const hre = require('hardhat')
const DUMMY_L1_ERC20_ADDRESS = NON_ZERO_ADDRESS
const DUMMY_L1_BRIDGE_ADDRESS = '0xACDCacDcACdCaCDcacdcacdCaCdcACdCAcDcaCdc'
describe('L2ECOApproveTransferRevert', () => {
const INITIAL_TOTAL_SUPPLY = 100000e18
const ALICE_INITIAL_BALANCE = ethers.utils.parseEther('50000')
let alice: SignerWithAddress
let bob: SignerWithAddress
let attacker: SignerWithAddress
let l2BridgeImpersonator: SignerWithAddress
let eco: L2ECO
const baseInflationMult = 1e18
beforeEach(async () => {
// Create a special signer which will enable us to send messages from the L2Messenger contract
;[alice, bob, l2BridgeImpersonator, attacker] = await ethers.getSigners()
const ecoFactory = await ethers.getContractFactory('L2ECO')
eco = (await upgrades.deployProxy(
ecoFactory,
[AddressZero, l2BridgeImpersonator.address],
{
initializer: 'initialize',
}
)) as L2ECO
await eco.deployed()
await expect(
eco
.connect(l2BridgeImpersonator)
.mint(alice.address, ALICE_INITIAL_BALANCE)
)
.to.emit(eco, 'Transfer')
.withArgs(AddressZero, alice.address, ALICE_INITIAL_BALANCE)
expect(await eco.balanceOf(alice.address)).to.equal(ALICE_INITIAL_BALANCE)
})
describe('approveTransfer', () => {
it('confirm initial inflation multiplier', async () => {
expect(await eco.linearInflationMultiplier()).to.equal(
baseInflationMult.toString()
)
})
it('confirm change of inflation multiplier', async () => {
//set rebase to 0.5
let newInflationMult = 0.5e18
await eco
.connect(l2BridgeImpersonator)
.rebase(newInflationMult.toString())
expect(await eco.linearInflationMultiplier()).to.equal(
newInflationMult.toString()
)
})
it('approveTransfer passes when no change of inflation multiplier', async () => {
await eco.connect(alice).approve(bob.address, ALICE_INITIAL_BALANCE)
await eco
.connect(bob)
.transferFrom(alice.address, bob.address, ALICE_INITIAL_BALANCE)
expect(await eco.balanceOf(bob.address)).to.equal(ALICE_INITIAL_BALANCE) ////when no change of inflation multiplier, approveTransfer passes
})
it('approveTransfer reverts when inflation multiplier increases and token approved prior to increase', async () => {
await eco.connect(alice).approve(bob.address, ALICE_INITIAL_BALANCE)
//set rebase to 2, scale balance in half
let newInflationMult = 2e18
await eco
.connect(l2BridgeImpersonator)
.rebase(newInflationMult.toString())
await expect(
eco
.connect(bob)
.transferFrom(alice.address, bob.address, ALICE_INITIAL_BALANCE)
).to.be.revertedWith(ERROR_STRINGS.L2ECO.TRANSFER_EXCEEDS_BALANCE) ////Transfer reverts due to change of inflation multiplier!Input amount is approved amount(50000), but balance is 25000
})
it('approveTransfer reverts when inflation multiplier increases due to MEV attacks', async () => {
//prepare attacker with initial funds
await expect(
eco
.connect(l2BridgeImpersonator)
.mint(attacker.address, ALICE_INITIAL_BALANCE)
)
////1. The attacker arrange their own transaction before the rebase, taking advantage of the old inflation multiplier
//// Note that both the approve and transferFrom could be in the same transaction.
await eco.connect(attacker).approve(bob.address, ALICE_INITIAL_BALANCE)
await eco
.connect(bob)
.transferFrom(attacker.address, bob.address, ALICE_INITIAL_BALANCE)
expect(await eco.balanceOf(bob.address)).to.equal(ALICE_INITIAL_BALANCE) //// attacker's transaction succeeds
////2. Rebase is executed in l2, scale balance in half
let newInflationMult = 2e18
await eco
.connect(l2BridgeImpersonator)
.rebase(newInflationMult.toString())
////3. The attacker arrange alice's transaction after the rebase, causing it to revert due to insufficient balance.
//// Note that both the approve and transferFrom could be in the same transaction
await eco.connect(alice).approve(bob.address, ALICE_INITIAL_BALANCE)
await expect(
eco
.connect(bob)
.transferFrom(alice.address, bob.address, ALICE_INITIAL_BALANCE)
).to.be.revertedWith(ERROR_STRINGS.L2ECO.TRANSFER_EXCEEDS_BALANCE)
//// Alice's transaction reverts due to change of inflation multiplier and MEV attack: input amount is approved amount(50000), but balance is 25000.
//// This kind of attack could happen in many contexts where L2ECO transfer is required. One exmaple is L2ECO trading on AMMs.
//// Although context might not be directly on ECO, the attack is enabled by a temporary break between two accounting systems both used in L2ECO contract.
//// In addition, the timing of the attack is predictable due the governance process of rebase. And it's likely profitable for the attacker, causing the victim to lose profits.
})
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment