git clone https://github.com/Frenchkebab/metamorphic-contracts.git
cd metamorphic-contracts
npm install
npx hardhat test test/ExploitConstructor/ExploitConstructor.test.js
contract ContractThatDoesNotDoWhatItLooksLike {
bytes public _bytecode;
constructor(uint256 bytecodeLength, bytes memory bytecode) {
assembly {
/*
< memory >
0x80: bytecodeLength (parameter)
0xa0: bytecode location
0xc0: bytecode length
0xe0: bytecode
...
*/
return(0xe0, bytecodeLength)
}
}
function add1(uint256 x) external pure returns (uint256) {
return x + 1; // returns x + 2;
}
}
ContractThatDoesNotDoWhatItLooksLike
actually deploys different contract than what it looks like.
In it's constructor
, it does so by returning the bytecode passed in as an argument.
See How to Exploit a Solidity Constructor for more detailed explanation.
pragma solidity 0.8.16;
contract StrangeV4 {
bool check1;
address private strangeContract;
bytes32 private codeHash;
uint256 private codeLength;
constructor() payable {
require(msg.value == 1 ether);
}
function initialize(address _contract) external {
require(_contract.code.length != 0, "target must be a contract");
codeHash = _contract.codehash;
strangeContract = _contract;
}
function success(address _contract) external {
require(_contract.code.length != 0, "must be a contract");
require(_contract == strangeContract, "must be the same contract");
require(_contract.codehash != codeHash, "contract isn't strange");
uint256 bal;
assembly {
bal := selfbalance()
}
payable(msg.sender).transfer(bal);
}
}
Condition1
:_contract
must be a contractCondition2
:_contract
must have same address asstrangeContract
(previous_contract
)Condition3
:codeHash
of_contract
must be different fromstrangeContract
'scodeHash
To pass require conditions in
success
success function after callinginitialize
, you must deploy a contract that has same contract address with different code
$ npx hardhat test test/StrangeV4/StrangeV4-solution1.test.js
Here, you use MetamorphicContractFactory
by 0age.
This deploys a contract with bytecode 0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3
.
If you specify the address of its implementation when calling deployMetamorphicContractFromExistingImplementation
,
this function will deploy a bytecode contract above and this will copy the code of the implementation contract specified.
-
Deploy
MetamorphicContractFactory
,Implementation1
andImplementation2
. -
Deploy a metamorphic contract that has code from
Implementation1
as an implementation. -
Call
initialize
with created metamorphic contract address. -
Call
killme
and selfdestruct the metamorphic contract. -
Redeploy a metamorphic contract with
Implementation2
as an implement. -
Call
success
with the same contract address.
$ npx hardhat test test/StrangeV4/StrangeV4-solution2.test.js
Checkout this repository
- Deploy
Factory
contract - Deploy
Parent
fromFactory
usingcreate2
- Deploy
Child1
fromParent
usingcreate
- call
initialize
withChild1
's address. selfdestruct
bothChild1
andParent
- Redeploy
Parent
fromFactory
- Deploy
Child2
fromParent
. - Call
success
withChild2
's address.
2. create2 3. create
1. Factory ----------------> Parent ----------------> Child1
5. 5.
6. create2 7. create
Factory ----------------> Parent ----------------> Child2
Parent
has same address when redeployed since we use same bytecode and salt for create2
.
But how can Child1
and Child2
have same address?
There is two factors that determines the address When you use create
:
sender
nonce
If you selfdestruct
Parent
contract, its nonce
is reset to 0
.
So when you deploy Child2
, it will have the very same address as when Child1
was deployed.