Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions contracts/bugs/DepositToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract DepositToken is ERC20("VaultToken", "VLT") {
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
15 changes: 15 additions & 0 deletions contracts/bugs/Forwarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Forwarder {
event Forwarded(address indexed target, bytes data);

function forward(address target, bytes calldata data)
external
returns (bool)
{
(bool success,) = target.call(data);
emit Forwarded(target, data);
return success;
}
}
17 changes: 17 additions & 0 deletions contracts/bugs/OwnedRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract OwnedRegistry is Ownable {
uint256 public value;

event ValueSet(uint256 value);

constructor(address initialOwner) Ownable(initialOwner) {}

function setValue(uint256 newValue) external onlyOwner {
value = newValue;
emit ValueSet(newValue);
}
}
14 changes: 14 additions & 0 deletions contracts/bugs/SixDecimalToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SixDecimalToken is ERC20("TestUSD", "TUSD") {
function decimals() public pure override returns (uint8) {
return 6;
}

function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
22 changes: 22 additions & 0 deletions contracts/bugs/Vault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Vault {
IERC20 public immutable token;

mapping(address => uint256) public deposits;

event Deposited(address indexed from, uint256 amount);

constructor(IERC20 token_) {
token = token_;
}

function deposit(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
deposits[msg.sender] += amount;
emit Deposited(msg.sender, amount);
}
}
46 changes: 46 additions & 0 deletions contracts/scripts/BugsDeploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";

import {OwnedRegistry} from "contracts/bugs/OwnedRegistry.sol";
import {SixDecimalToken} from "contracts/bugs/SixDecimalToken.sol";
import {Vault} from "contracts/bugs/Vault.sol";
import {DepositToken} from "contracts/bugs/DepositToken.sol";
import {Forwarder} from "contracts/bugs/Forwarder.sol";

// Seeds the broken on-chain state the MCP debug demo relies on.
// See docs/mcp-debug-runbook.md.
contract BugsDeployScript is Script {
address alice = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
address bob = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8);

function run() public {
vm.startBroadcast();

// Scenario 2: owned by bob, while ethui's active account is alice.
OwnedRegistry registry = new OwnedRegistry(bob);
console.log("OwnedRegistry", address(registry));

// Scenario 3: 6 decimals, funded so the balance reads as a round 100.
SixDecimalToken sixDecimalToken = new SixDecimalToken();
sixDecimalToken.mint(alice, 100e6);
sixDecimalToken.mint(bob, 100e6);
console.log("SixDecimalToken", address(sixDecimalToken));

// Scenario 4: funded accounts, deliberately zero approvals.
DepositToken depositToken = new DepositToken();
depositToken.mint(alice, 100e18);
depositToken.mint(bob, 100e18);
Vault vault = new Vault(depositToken);
console.log("DepositToken", address(depositToken));
console.log("Vault", address(vault));

// Scenario 5: forwards into the bob-owned registry, so the inner call
// always reverts and is swallowed.
Forwarder forwarder = new Forwarder();
console.log("Forwarder", address(forwarder));

vm.stopBroadcast();
}
}
44 changes: 44 additions & 0 deletions contracts/test/bugs/Forwarder.d.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// INVERTED TEST: this passes while the demo bug is present. Do not "fix" it.
// Scenario 5 depends on forward() swallowing a failed inner call: the outer
// transaction succeeds and emits Forwarded even though nothing changed.

import {Test} from "forge-std/Test.sol";

import {Forwarder} from "contracts/bugs/Forwarder.sol";
import {OwnedRegistry} from "contracts/bugs/OwnedRegistry.sol";

contract ForwarderTest is Test {
address alice = address(0xA11CE);
address bob = address(0xB0B);

Forwarder forwarder;
OwnedRegistry registry;

function setUp() public {
forwarder = new Forwarder();
registry = new OwnedRegistry(bob);
}

function test_forward_swallowsFailure() public {
bytes memory data = abi.encodeCall(OwnedRegistry.setValue, (42));

vm.prank(alice);
bool success = forwarder.forward(address(registry), data);

assertFalse(success);
assertEq(registry.value(), 0);
}

function test_forward_emitsEvenWhenInnerCallFails() public {
bytes memory data = abi.encodeCall(OwnedRegistry.setValue, (42));

vm.expectEmit(true, false, false, true);
emit Forwarder.Forwarded(address(registry), data);

vm.prank(alice);
forwarder.forward(address(registry), data);
}
}
35 changes: 35 additions & 0 deletions contracts/test/bugs/OwnedRegistry.d.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// INVERTED TEST: this passes while the demo bug is present. Do not "fix" it.
// Scenario 2 depends on setValue reverting for a non-owner caller.

import {Test} from "forge-std/Test.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

import {OwnedRegistry} from "contracts/bugs/OwnedRegistry.sol";

contract OwnedRegistryTest is Test {
address alice = address(0xA11CE);
address bob = address(0xB0B);

OwnedRegistry registry;

function setUp() public {
registry = new OwnedRegistry(bob);
}

function test_setValue_revertsForNonOwner() public {
vm.prank(alice);
vm.expectRevert(
abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)
);
registry.setValue(42);
}

function test_setValue_succeedsForOwner() public {
vm.prank(bob);
registry.setValue(42);
assertEq(registry.value(), 42);
}
}
35 changes: 35 additions & 0 deletions contracts/test/bugs/SixDecimalToken.d.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// INVERTED TEST: this passes while the demo bug is present. Do not "fix" it.
// Scenario 3 depends on this token reporting 6 decimals, not 18.

import {Test} from "forge-std/Test.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";

import {SixDecimalToken} from "contracts/bugs/SixDecimalToken.sol";

contract SixDecimalTokenTest is Test {
address alice = address(0xA11CE);

SixDecimalToken token;

function setUp() public {
token = new SixDecimalToken();
token.mint(alice, 100e6);
}

function test_decimals_isSix() public view {
assertEq(token.decimals(), 6);
}

function test_transfer_revertsWhenAmountUsesEighteenDecimals() public {
vm.prank(alice);
vm.expectRevert(
abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientBalance.selector, alice, 100e6, 100e18
)
);
token.transfer(address(0xB0B), 100e18);
}
}
43 changes: 43 additions & 0 deletions contracts/test/bugs/Vault.d.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// INVERTED TEST: this passes while the demo bug is present. Do not "fix" it.
// Scenario 4 depends on deposit reverting when the caller has not approved.

import {Test} from "forge-std/Test.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";

import {Vault} from "contracts/bugs/Vault.sol";
import {DepositToken} from "contracts/bugs/DepositToken.sol";

contract VaultTest is Test {
address alice = address(0xA11CE);

DepositToken token;
Vault vault;

function setUp() public {
token = new DepositToken();
vault = new Vault(token);
token.mint(alice, 100e18);
}

function test_deposit_revertsWithoutApproval() public {
vm.prank(alice);
vm.expectRevert(
abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientAllowance.selector, address(vault), 0, 10e18
)
);
vault.deposit(10e18);
}

function test_deposit_succeedsAfterApproval() public {
vm.startPrank(alice);
token.approve(address(vault), 10e18);
vault.deposit(10e18);
vm.stopPrank();

assertEq(vault.deposits(alice), 10e18);
}
}
Loading
Loading