팩토리 패턴에 대한 글에서 new라는 키워드가 CREATE opcode를 호출한다는 얘기를 했었다.
자식 컨트랙트를 생성하는 opcode에는 이외에도 CREATE2가 있다.
각자가 어떻게 다른 지 알아보자.
0xF0에 해당하는 opcode로 creates a child contract의 설명처럼 자식 컨트랙트를 생성할 때 사용한다.
msg.sender의 주소와 nonce를 사용해서 컨트랙트 주소를 만든다.
호출자의 nonce는 계속 증가하므로 매번 새로운 컨트랙트 주소가 생성된다.
따라서 주소가 충돌하지 않는다.
go-ethereum/core/vm/evm.go
// Create creates a new contract using code as deployment code. func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address())) return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE) }
콘스탄티노플 업그레이드의 EIP-1014의 opcode로 0xF5로 추가되었다.
creates a child contract with a deterministic address라는 설명에서 알 수 있듯이, 주소를 특정하여 자식 컨트랙트를 생성할 수 있다.
이는 Create와 달리 nonce를 쓰지 않고 0xff, salt, init_code를 사용하기에 가능하다.
go-ethereum/core/vm/evm.go
// Create2 creates a new contract using code as deployment code. // // The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:] // instead of the usual sender-and-nonce-hash as the address where the contract is initialized at. func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *big.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { codeAndHash := &codeAndHash{code: code} contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes()) return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2) }
기존에 생성한 컨트랙트의 주소와 충돌 문제가 생길 수 있다.
Create에 의해 생성된 컨트랙트 주소는 20 바이트의 address와 8 바이트의 nonce로 이뤄진 배열이 RLP 인코딩을 거쳐 나온다.
이 값은 0xff의 prefix를 가질 수 없다.
go-ethereum/crypto/crypto.go
// CreateAddress creates an ethereum address given the bytes and the nonce func CreateAddress(b common.Address, nonce uint64) common.Address { data, _ := rlp.EncodeToBytes([]interface{}{b, nonce}) return common.BytesToAddress(Keccak256(data)[12:]) }
반면, Create2에 의해 생성된 컨트랙트 주소는 0xff의 prefix를 가진다.
go-ethereum/crypto/crypto.go
// CreateAddress2 creates an ethereum address given the address bytes, initial // contract code hash and a salt. func CreateAddress2(b common.Address, salt [32]byte, inithash []byte) common.Address { return common.BytesToAddress(Keccak256([]byte{0xff}, b.Bytes(), salt[:], inithash)[12:]) }
따라서 Create로 생성된 주소와 Create2로 생성된 주소는 충돌하지 않는다.
그렇다면 이미 존재하는 주소로 재배포하는 경우에는 어떻게 될까?
이 경우, nonce가 0이 아니라면(=이미 존재하는 주소라면) 에러를 반환한다.
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { ... // Ensure there's no existing contract already at the designated address contractHash := evm.StateDB.GetCodeHash(address) if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) { return nil, common.Address{}, 0, ErrContractAddressCollision } ... }
따라서 동일한 주소로 재배포를 할 경우에는 selfdestruct를 호출해서 컨트랙트를 체인에서 삭제한 후 생성하면 되겠다.
Zombie.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
contract Zombie {
string public name;
uint public dna;
constructor(string memory _name, uint _dna) {
name = _name;
dna = _dna;
}
function destroy(address payable recipient) public {
selfdestruct(recipient);
}
}
팩토리 패턴에서 destory만 추가되었다.
찾다보니 안 사실인데 EIP-4758, EIP-6049에서 selfdesturct가 sendall로 대체된다거나, 사용되지 않는다는 말이 있다.
이로 인해 Solidity 0.8.18부터는 사용되지 않고, compile시 경고가 뜬다.
ZombieFactory.sol
function _createZombieV2(string memory _name, uint _dna) private {
address zombie;
bytes32 salt = keccak256(abi.encodePacked(_name));
bytes memory bytecode = abi.encodePacked(
type(Zombie).creationCode,
abi.encode(_name, _dna)
);
assembly {
zombie := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
if iszero(extcodesize(zombie)) {
revert(0, 0)
}
}
zombies.push(Zombie(zombie));
emit Created(zombie, _dna);
}
function createRandomZombieV2(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombieV2(_name, randDna);
}
salt의 타입은 bytes32고, 예시에서는 name을 사용했다.
생성자에 필요한 인자를 넣기 위해서 create2전에 abi-encode를 해야한다.
test.js
contract("ZombieFactory", async (accounts) => {
const {
bytecode: zombieBytecode,
} = require("../build/contracts/Zombie.json");
const name1 = "Gildong";
const name2 = "Chulsoo";
before(async () => {
zombieFactory = await Factory.new({ from: accounts[0] });
console.log("Deployed Factory: ", zombieFactory.address);
});
...
});
bytecode은 off-chain에서 주소를 생성하기 위해 필요하다.
좀비의 이름은 salt로 쓰인다.
before 함수를 통해 테스트 진행 전에 팩토리 컨트랙트를 배포할 수 있다.
create를 통해 생성시 자식 컨트랙트의 주소는 매번 달라져야 한다.
describe("Zombie generated by 'create'", () => {
it("두 좀비의 주소가 달라야 합니다.", async () => {
let tx = await zombieFactory.createRandomZombie(name1);
var { zombie, dna } = tx.logs[0].args;
const zombie1Addr = zombie;
const zombie1 = await Zombie.at(zombie1Addr);
let name = await zombie1.name();
console.log("Zombie generated:", zombie1Addr, name, +dna);
tx = await zombieFactory.createRandomZombie(name2);
var { zombie, dna } = tx.logs[0].args;
const zombie2Addr = zombie;
const zombie2 = await Zombie.at(zombie2Addr);
name = await zombie2.name();
console.log("Zombie generated:", zombie2Addr, name, +dna);
await assert.notStrictEqual(zombie1Addr, zombie2Addr);
});
});
describe("Zombie generated by 'create2'", () => {
let tx, zombie1Addr, name, zombieDNA, zombie1;
beforeEach(async () => {
tx = await zombieFactory.createRandomZombieV2(name1);
var { zombie, dna } = tx.logs[0].args;
zombie1Addr = zombie;
zombie1 = await Zombie.at(zombie1Addr);
name = await zombie1.name();
zombieDNA = +dna;
console.log("Zombie generated:", zombie1Addr, name, zombieDNA);
});
afterEach(async () => {
zombie1 = await Zombie.at(zombie1Addr);
truffleAssert.passes(await zombie1.destroy(accounts[0]));
});
beforeEach는 테스트 케이스를 실행할 때마다 선행되는 함수로, 여기서는 테스트 케이스마다 Gildong이라는 이름의 좀비를 create2로 생성하게 된다.
매 케이스마다 같은 주소의 컨트랙트를 생성하지 않도록 afterEach를 통해 기존의 함수를 없앤다.
salt가 같아 기존의 주소와 충돌하는 것을 확인하기 위한 테스트it("이름이 같은 좀비를 생성시 실패해야 합니다.", async () => {
await catchRevert(zombieFactory.createRandomZombieV2(name1));
});
it("좀비의 주소가 오프체인에서 특정한 주소와 같아야 합니다.", async () => {
zombie1 = await Zombie.at(zombie1Addr);
dna = +(await zombie1.dna());
dnaString = dna.toString();
name = await zombie1.name();
const params = encodeParams(["string", "uint"], [name, dnaString]);
const bytecode = `${zombieBytecode}${params.slice(2)}`;
assert.equal(await zombieFactory.getBytecode(name, dna), bytecode);
const salt = nameToSalt(name1);
assert.equal(await zombieFactory.getSalt(name), salt);
const computeAddr = createAddress2(zombieFactory.address, name, bytecode);
assert.equal(zombie1Addr.toLowerCase(), computeAddr);
});
it("이름이 다른 두 좀비의 주소가 달라야 합니다.", async () => {
tx = await zombieFactory.createRandomZombieV2(name2);
var { zombie, dna } = tx.logs[0].args;
zombie2Addr = zombie;
zombie2 = await Zombie.at(zombie2Addr);
name = await zombie2.name();
assert.strictEqual(name, name2);
assert.notStrictEqual(zombie1Addr, zombie2Addr);
});

https://eips.ethereum.org/EIPS/eip-1014
https://ethervm.io
https://github.com/ethereum/go-ethereum
https://ethereum.stackexchange.com/questions/101336/what-is-the-benefit-of-using-create2-to-create-a-smart-contract
https://ethereum.stackexchange.com/questions/78738/passing-constructor-arguments-to-the-create-assembly-instruction-in-solidity
안녕하세요 글 잘봤습니다. 이더리움 코어는 따로 자료를 찾아서 공부하신건가요!?