Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,123 @@ runs:
main: 'dist/setup/index.js'
post: 'dist/cache-save/index.js'
post-if: success()
&& // SPDX-License-Identifier: MIT
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solidity contract code is incorrectly added to a YAML file (action.yml). This file is a GitHub Action configuration for Node.js setup, not a Solidity contract repository. The contract should be placed in a separate .sol file in an appropriate directory (e.g., contracts/WrappedTestnetBTC.sol), and this entire addition to action.yml should be removed.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 44 starts with '&&' which is invalid syntax for both YAML and Solidity. This will cause parsing errors when the action.yml file is processed by GitHub Actions.

Copilot uses AI. Check for mistakes.
pragma solidity ^0.8.20;

contract WrappedTestnetBTC {
string public constant name = "Wrapped Testnet Bitcoin";
string public constant symbol = "wTBTC";
uint8 public constant decimals = 18;
uint256 public totalSupply;

address public bridgeOperator;
bool public paused = false;

mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event Mint(address indexed to, uint256 amount, string bitcoinTxId);
event Burn(address indexed from, uint256 amount, string bitcoinAddress);
event BridgeOperatorChanged(address indexed oldOperator, address indexed newOperator);
event Paused(bool status);

modifier onlyBridgeOperator() {
require(msg.sender == bridgeOperator, "Only bridge operator");
_;
}

modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}

constructor(address _initialOperator) {
bridgeOperator = _initialOperator;
emit BridgeOperatorChanged(address(0), _initialOperator);
}

function mint(address to, uint256 amount, string calldata bitcoinTxId)
external
onlyBridgeOperator
whenNotPaused
{
require(to != address(0), "Mint to zero address");
require(amount > 0, "Amount must be positive");
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mint function allows minting zero tokens to pass the check on line 87, but then still emits events and updates state. While line 87 checks for 'amount > 0', an amount of 0 would still fail this check. However, the broader issue is this entire contract should not be in action.yml at all.

Copilot uses AI. Check for mistakes.

balanceOf[to] += amount;
totalSupply += amount;
Comment on lines +89 to +90
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mint function lacks protection against integer overflow in Solidity 0.8.20, though Solidity 0.8+ has built-in overflow protection. However, there's no maximum supply cap, which could allow unlimited minting by the bridge operator. Consider adding a max supply limit for security.

Copilot uses AI. Check for mistakes.

emit Transfer(address(0), to, amount);
emit Mint(to, amount, bitcoinTxId);
}

function burn(uint256 amount, string calldata bitcoinAddress)
external
whenNotPaused
{
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
require(bytes(bitcoinAddress).length > 0, "Bitcoin address required");
Comment on lines +95 to +101
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The burn function only validates that bitcoinAddress is non-empty but doesn't validate its format or correctness. This could lead to tokens being burned with invalid Bitcoin addresses, resulting in permanent loss of funds. Consider adding proper Bitcoin address format validation.

Suggested change
function burn(uint256 amount, string calldata bitcoinAddress)
external
whenNotPaused
{
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
require(bytes(bitcoinAddress).length > 0, "Bitcoin address required");
function isValidBitcoinAddress(string memory bitcoinAddress) internal pure returns (bool) {
bytes memory addrBytes = bytes(bitcoinAddress);
uint256 len = addrBytes.length;
// Typical Bitcoin address lengths (legacy, P2SH, Bech32)
if (len < 26 || len > 62) {
return false;
}
// Check common mainnet and testnet prefixes:
// '1', '3', 'm', 'n', '2', 'bc1', 'tb1'
bool hasValidPrefix = false;
if (
addrBytes[0] == bytes1("1") ||
addrBytes[0] == bytes1("3") ||
addrBytes[0] == bytes1("m") ||
addrBytes[0] == bytes1("n") ||
addrBytes[0] == bytes1("2")
) {
hasValidPrefix = true;
} else if (len >= 3) {
if (
addrBytes[0] == bytes1("b") &&
addrBytes[1] == bytes1("c") &&
addrBytes[2] == bytes1("1")
) {
hasValidPrefix = true;
} else if (
addrBytes[0] == bytes1("t") &&
addrBytes[1] == bytes1("b") &&
addrBytes[2] == bytes1("1")
) {
hasValidPrefix = true;
}
}
if (!hasValidPrefix) {
return false;
}
// Ensure all characters are alphanumeric (reject spaces and symbols)
for (uint256 i = 0; i < len; i++) {
bytes1 c = addrBytes[i];
bool isDigit = (c >= bytes1("0") && c <= bytes1("9"));
bool isUpper = (c >= bytes1("A") && c <= bytes1("Z"));
bool isLower = (c >= bytes1("a") && c <= bytes1("z"));
if (!(isDigit || isUpper || isLower)) {
return false;
}
}
return true;
}
function burn(uint256 amount, string calldata bitcoinAddress)
external
whenNotPaused
{
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
require(isValidBitcoinAddress(bitcoinAddress), "Invalid Bitcoin address");

Copilot uses AI. Check for mistakes.

balanceOf[msg.sender] -= amount;
totalSupply -= amount;

emit Transfer(msg.sender, address(0), amount);
emit Burn(msg.sender, amount, bitcoinAddress);
}

function approve(address spender, uint256 amount)
external
whenNotPaused
returns (bool)
{
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approve function is vulnerable to the ERC20 approval race condition. When changing an approval from a non-zero value to another non-zero value, an attacker could spend both the old and new amounts. Consider implementing increaseAllowance/decreaseAllowance functions or requiring the allowance to be set to zero first.

Suggested change
{
{
require(spender != address(0), "Approve to zero address");
// Mitigate ERC20 approval race condition by enforcing zero-first updates
require(
amount == 0 || allowance[msg.sender][spender] == 0,
"Non-zero to non-zero allowance change not allowed"
);

Copilot uses AI. Check for mistakes.
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

function transfer(address to, uint256 amount)
external
whenNotPaused
returns (bool)
{
_transfer(msg.sender, to, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount)
external
whenNotPaused
returns (bool)
{
require(allowance[from][msg.sender] >= amount, "Allowance exceeded");

allowance[from][msg.sender] -= amount;
_transfer(from, to, amount);

return true;
}

function changeBridgeOperator(address newOperator) external onlyBridgeOperator {
require(newOperator != address(0), "Zero address");
emit BridgeOperatorChanged(bridgeOperator, newOperator);
bridgeOperator = newOperator;
}
Comment on lines +142 to +146
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changeBridgeOperator function lacks a two-step transfer mechanism. If the newOperator address is incorrect or compromised, control of the contract cannot be recovered. Consider implementing a two-step transfer process with acceptance by the new operator.

Suggested change
function changeBridgeOperator(address newOperator) external onlyBridgeOperator {
require(newOperator != address(0), "Zero address");
emit BridgeOperatorChanged(bridgeOperator, newOperator);
bridgeOperator = newOperator;
}
address public pendingBridgeOperator;
event BridgeOperatorChangeInitiated(address indexed currentOperator, address indexed newOperator);
modifier onlyPendingBridgeOperator() {
require(msg.sender == pendingBridgeOperator, "Not pending operator");
_;
}
function changeBridgeOperator(address newOperator) external onlyBridgeOperator {
require(newOperator != address(0), "Zero address");
pendingBridgeOperator = newOperator;
emit BridgeOperatorChangeInitiated(bridgeOperator, newOperator);
}
function acceptBridgeOperator() external onlyPendingBridgeOperator {
emit BridgeOperatorChanged(bridgeOperator, pendingBridgeOperator);
bridgeOperator = pendingBridgeOperator;
pendingBridgeOperator = address(0);
}

Copilot uses AI. Check for mistakes.

function setPaused(bool _paused) external onlyBridgeOperator {
paused = _paused;
emit Paused(_paused);
}

function _transfer(address from, address to, uint256 amount) internal {
require(from != address(0), "Transfer from zero address");
require(to != address(0), "Transfer to zero address");
require(balanceOf[from] >= amount, "Insufficient balance");

balanceOf[from] -= amount;
balanceOf[to] += amount;

emit Transfer(from, to, amount);
}
}
Loading