In this contract, I call payWinner()
once the third participant has bought a ticket. payWinner()
is internal to avoid any external call but when it executes winnerAddress.transfer(winnableBalance);
, the contract balance is sent to an internal address rather than to the external address winnerAddress
. Same thing goes for burnLeftBalance()
obviously.
I guess either it's impossible to transfer part of the contract holdings to an external address from an internal function and I need to create a public function for that (but I don't see how I can be sure that it's the contract itself calling the function), or there is something I'm missing.
pragma solidity ^0.8.14;
contract Lottery {
address payable public burnAddress = payable(0x0000000000000000000000000000000000000000);
uint256 public ticketsPerRound = 3;
uint256 public ticketPrice = 0.001 * 10e8 * 10e8;
address[] public participantAddresses;
error WrongTicketPrice(uint256 expectedRole, uint256 actualRole);
function buyTicket() external payable {
if (msg.value != ticketPrice) {
revert WrongTicketPrice(ticketPrice, msg.value);
}
participantAddresses.push(msg.sender);
if (participantAddresses.length >= ticketsPerRound) {
payWinner();
}
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function getParticipantAddresses() external view returns (address[] memory) {
return participantAddresses;
}
function getWinnableBalance() public view returns (uint256) {
return address(this).balance / 2;
}
function getWinner() internal view returns (address payable winnerAddress) {
uint256 unsafeRandomNumber = uint256(
keccak256(abi.encodePacked(block.difficulty, block.timestamp, participantAddresses))
);
uint256 winnerIndex = unsafeRandomNumber % participantAddresses.length;
winnerAddress = payable(participantAddresses[winnerIndex]);
}
function burnLeftBalance() internal {
uint256 leftBalance = address(this).balance;
burnAddress.transfer(leftBalance);
reset();
}
function payWinner() internal {
uint256 winnableBalance = getWinnableBalance();
address payable winnerAddress = getWinner();
winnerAddress.transfer(winnableBalance);
burnLeftBalance();
}
function reset() internal {
delete participantAddresses;
}
}
There's no such thing as "internal and external addresses". The closest to this term I can think of is Externally Owned Account, which is a term representing an address derived from a private key, without a contract bytecode, mostly used by end user wallets. But that does not seem to be related to your question.
Etherscan is using "internal transactions" as a more user-friedly term for "message calls within the virtual machine". These "internal transaction" details (such as from what sender, to what recipient, what data was passed, what ETH value, ...) are not part of the public blockchain data - only the combined state changes of the "parent" transaction are stored in the blockchain. So Etherscan retrieves these details by emulating the transaction, records its message calls and stores them in Etherscan's own database, and then displays them in a separate tab.
So, the transaction that you sent is from
your address, to
the contract address, with value
of 0.01 ETH, its data
field contains selector of the buyTicket()
function that you want to execute... These are all params of the transaction that you sent. And the "internal transactions" are just a way of displaying state changes that your transaction produced, and their trace. But as long as the state changes were produced (e.g. increased balance of the winnerAddress
), there's no difference if they were produced by a "parent" transaction or an "internal transaction".
TLDR: It's not a bug, it's a feature. :)