@airdao/astra-universal-router
Version:
Smart contracts for Universal Router
230 lines (200 loc) • 9.02 kB
text/typescript
import {
UniversalRouter,
Permit2,
ERC20,
ISAMB,
MockLooksRareRewardsDistributor,
ERC721,
MintableERC20__factory,
ISAMB__factory,
} from '../../typechain'
import { BigNumber, BigNumberish } from 'ethers'
import { Pair } from '@airdao/astra-classic-sdk'
import { expect } from './shared/expect'
import { abi as ROUTER_ABI } from '../../artifacts/contracts/UniversalRouter.sol/UniversalRouter.json'
import { abi as TOKEN_ABI } from '../../artifacts/solmate/src/tokens/ERC20.sol/ERC20.json'
import { abi as SAMB_ABI } from '../../artifacts/contracts/interfaces/external/ISAMB.sol/ISAMB.json'
import deployUniversalRouter, { deployPermit2 } from './shared/deployUniversalRouter'
import {
ADDRESS_THIS,
ALICE_ADDRESS,
DEADLINE,
ROUTER_REWARDS_DISTRIBUTOR,
SOURCE_MSG_SENDER,
MAX_UINT160,
MAX_UINT,
AMB_ADDRESS,
} from './shared/constants'
import { resetFork, SAMB, BOND } from './shared/testnetForkHelpers'
import { CommandType, RoutePlanner } from './shared/planner'
import { makePair } from './shared/swapRouter02Helpers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expandTo18DecimalsBN } from './shared/helpers'
import hre from 'hardhat'
import { findCustomErrorSelector } from './shared/parseEvents'
import { Token } from '@airdao/astra-sdk-core'
const { ethers } = hre
const routerInterface = new ethers.utils.Interface(ROUTER_ABI)
describe('UniversalRouter', () => {
let alice: SignerWithAddress
let router: UniversalRouter
let permit2: Permit2
let bondContract: ERC20
let sambContract: ISAMB
let mockLooksRareToken: ERC20
let mockLooksRareRewardsDistributor: MockLooksRareRewardsDistributor
let pair_BOND_SAMB: Pair
beforeEach(async () => {
await resetFork()
alice = await ethers.getSigner(ALICE_ADDRESS)
await hre.network.provider.request({
method: 'hardhat_impersonateAccount',
params: [ALICE_ADDRESS],
})
await hre.network.provider.request({
method: 'hardhat_setBalance',
params: [ALICE_ADDRESS, BigNumber.from(1_000_000).mul(BigNumber.from(10).pow(18))._hex],
})
// mock rewards contracts
const tokenFactory = await ethers.getContractFactory('MintableERC20')
const mockDistributorFactory = await ethers.getContractFactory('MockLooksRareRewardsDistributor')
mockLooksRareToken = (await tokenFactory.connect(alice).deploy('LooksRare', 'LR', expandTo18DecimalsBN(5))) as ERC20
mockLooksRareRewardsDistributor = (await mockDistributorFactory.deploy(
ROUTER_REWARDS_DISTRIBUTOR,
mockLooksRareToken.address
)) as MockLooksRareRewardsDistributor
await (await ISAMB__factory.connect(SAMB.address, alice).deposit({ value: expandTo18DecimalsBN(1000) })).wait()
bondContract = new ethers.Contract(BOND.address, TOKEN_ABI, alice) as ERC20
sambContract = new ethers.Contract(SAMB.address, SAMB_ABI, alice) as ISAMB
pair_BOND_SAMB = await makePair(alice, BOND, SAMB)
permit2 = (await deployPermit2()).connect(alice) as Permit2
router = (
await deployUniversalRouter(permit2, mockLooksRareRewardsDistributor.address, mockLooksRareToken.address)
).connect(alice) as UniversalRouter
})
describe('#execute', () => {
let planner: RoutePlanner
const invalidCommand: string = '0x3f'
beforeEach(async () => {
planner = new RoutePlanner()
await bondContract.approve(permit2.address, MAX_UINT)
await sambContract.approve(permit2.address, MAX_UINT)
await permit2.approve(BOND.address, router.address, MAX_UINT160, DEADLINE)
await permit2.approve(SAMB.address, router.address, MAX_UINT160, DEADLINE)
})
it('reverts if block.timestamp exceeds the deadline', async () => {
planner.addCommand(CommandType.CLASSIC_SWAP_EXACT_IN, [
alice.address,
1,
1,
[],
SOURCE_MSG_SENDER,
])
const invalidDeadline = 10
const { commands, inputs } = planner
await expect(
router['execute(bytes,bytes[],uint256)'](commands, inputs, invalidDeadline)
).to.be.revertedWithCustomError(router, 'TransactionDeadlinePassed')
})
it('reverts for an invalid command at index 0', async () => {
const inputs: string[] = ['0x12341234']
await expect(router['execute(bytes,bytes[],uint256)'](invalidCommand, inputs, DEADLINE))
.to.be.revertedWithCustomError(router, 'InvalidCommandType')
.withArgs(parseInt(invalidCommand))
})
it('reverts for an invalid command at index 1', async () => {
planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [
BOND.address,
pair_BOND_SAMB.liquidityToken.address,
expandTo18DecimalsBN(1),
])
let commands = planner.commands
let inputs = planner.inputs
commands = commands.concat(invalidCommand.slice(2))
inputs.push('0x21341234')
await expect(router['execute(bytes,bytes[],uint256)'](commands, inputs, DEADLINE))
.to.be.revertedWithCustomError(router, 'InvalidCommandType')
.withArgs(parseInt(invalidCommand))
})
it('reverts if paying a portion over 100% of contract balance', async () => {
await bondContract.transfer(router.address, expandTo18DecimalsBN(1))
planner.addCommand(CommandType.PAY_PORTION, [SAMB.address, alice.address, 11_000])
planner.addCommand(CommandType.SWEEP, [SAMB.address, alice.address, 1])
const { commands, inputs } = planner
await expect(router['execute(bytes,bytes[])'](commands, inputs)).to.be.revertedWithCustomError(
router,
'InvalidBips'
)
})
it('reverts if a malicious contract tries to reenter', async () => {
const reentrantProtocol = await (await ethers.getContractFactory('ReenteringProtocol')).deploy()
router = (
await deployUniversalRouter(
permit2,
mockLooksRareRewardsDistributor.address,
mockLooksRareToken.address,
reentrantProtocol.address
)
).connect(alice) as UniversalRouter
planner.addCommand(CommandType.SWEEP, [AMB_ADDRESS, alice.address, 0])
let { commands, inputs } = planner
const sweepCalldata = routerInterface.encodeFunctionData('execute(bytes,bytes[])', [commands, inputs])
const reentrantCalldata = reentrantProtocol.interface.encodeFunctionData('callAndReenter', [
router.address,
sweepCalldata,
])
planner = new RoutePlanner()
planner.addCommand(CommandType.NFTX, [0, reentrantCalldata])
;({ commands, inputs } = planner)
const customErrorSelector = findCustomErrorSelector(reentrantProtocol.interface, 'NotAllowedReenter')
await expect(router['execute(bytes,bytes[])'](commands, inputs))
.to.be.revertedWithCustomError(router, 'ExecutionFailed')
.withArgs(0, customErrorSelector)
})
})
describe('#collectRewards', () => {
let amountRewards: BigNumberish
beforeEach(async () => {
amountRewards = expandTo18DecimalsBN(0.5)
mockLooksRareToken.connect(alice).transfer(mockLooksRareRewardsDistributor.address, amountRewards)
})
it('transfers owed rewards into the distributor contract', async () => {
const balanceBefore = await mockLooksRareToken.balanceOf(ROUTER_REWARDS_DISTRIBUTOR)
await router.collectRewards('0x00')
const balanceAfter = await mockLooksRareToken.balanceOf(ROUTER_REWARDS_DISTRIBUTOR)
expect(balanceAfter.sub(balanceBefore)).to.eq(amountRewards)
})
})
})
describe('UniversalRouter newer block', () => {
let alice: SignerWithAddress
let router: UniversalRouter
let permit2: Permit2
let mockLooksRareToken: ERC20
let mockLooksRareRewardsDistributor: MockLooksRareRewardsDistributor
beforeEach(async () => {
// Since new NFTX contract was recently released, we have to fork from a much newer block
await resetFork(2765037) // 2765038 - 1
alice = await ethers.getSigner(ALICE_ADDRESS)
await hre.network.provider.request({
method: 'hardhat_impersonateAccount',
params: [ALICE_ADDRESS],
})
await hre.network.provider.request({
method: 'hardhat_setBalance',
params: [ALICE_ADDRESS, BigNumber.from(1_000_000).mul(BigNumber.from(10).pow(18))._hex],
})
// mock rewards contracts
const tokenFactory = await ethers.getContractFactory('MintableERC20')
const mockDistributorFactory = await ethers.getContractFactory('MockLooksRareRewardsDistributor')
mockLooksRareToken = (await tokenFactory.connect(alice).deploy(expandTo18DecimalsBN(5))) as ERC20
mockLooksRareRewardsDistributor = (await mockDistributorFactory.deploy(
ROUTER_REWARDS_DISTRIBUTOR,
mockLooksRareToken.address
)) as MockLooksRareRewardsDistributor
permit2 = (await deployPermit2()).connect(alice) as Permit2
router = (
await deployUniversalRouter(permit2, mockLooksRareRewardsDistributor.address, mockLooksRareToken.address)
).connect(alice) as UniversalRouter
})
})