Skip to content
Merged
Show file tree
Hide file tree
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
23 changes: 23 additions & 0 deletions solidity/supra_contracts/script/DeployERC20Supra.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import {Script, console} from "forge-std/Script.sol";
import {ERC20Supra} from "../src/ERC20Supra.sol";

contract DeployERC20Supra is Script {
address owner;

function setUp() public {
owner = vm.envAddress("OWNER");
}

function run() public {
vm.startBroadcast();

// Deploy ERC20Supra
ERC20Supra erc20Supra = new ERC20Supra(owner);
console.log("ERC20Supra deployed at: ", address(erc20Supra));

vm.stopBroadcast();
}
}
70 changes: 70 additions & 0 deletions solidity/supra_contracts/src/ERC20Supra.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";

contract ERC20Supra is ERC20, ERC20Burnable, Ownable2Step, ERC20Permit {

Choose a reason for hiding this comment

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

@udityadav-supraoracles I do not see method to delegate ERC20 to other address etc, are those functionalities handled in Parent contract?

Choose a reason for hiding this comment

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

Yes, if a user wants to delegate the authority to spend their ERC20 they can call approve which is defined in parent ERC20.


/// @notice Error thrown if user has insufficient balance.
error InsufficientBalance();
/// @notice Error thrown if 0 is passed as amount.
error InvalidAmount();
/// @notice Error thrown if tokens are sent to the token contract itself.
error InvalidTransfer();
/// @notice Error thrown if low level call fails.
error TransferFailed();

/// @notice Emitted when native tokens are deposited to mint and receive ERC20Supra tokens.
/// @param account Address of the depositer.
/// @param amount Amount deposited.
event NativeToERC20Supra(address indexed account, uint256 indexed amount);

/// @notice Emitted when native tokens are withdrawn by burning ERC20Supra tokens.
/// @param account Address withdrawing.
/// @param amount Amount withdrawn.
event ERC20SupraToNative(address indexed account, uint256 indexed amount);

constructor(address _initialOwner)
ERC20("ERC20Supra", "SUPRA")
Ownable(_initialOwner)
ERC20Permit("ERC20Supra")
{}

/// @notice Deposit native token → Mint ERC20Supra 1:1
function nativeToErc20Supra() external payable {
if (msg.value == 0) revert InvalidAmount();
_mint(msg.sender, msg.value);
Comment on lines +38 to +39

Choose a reason for hiding this comment

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

does msg.value implicitly deducts money from sender balance?

Choose a reason for hiding this comment

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

Since, it's a payable function they're sending the money along with the tx. msg.value only refers to that amount being sent. Here, it'll mint ERC20Supra tokens equivalent to msg.value i.e. 1:1.


emit NativeToERC20Supra(msg.sender, msg.value);
}

/// @notice Withdraw native token → Burn ERC20Supra 1:1
/// @param _amount Amount of native tokens to withdraw.
function erc20SupraToNative(uint256 _amount) external {
if (_amount == 0) revert InvalidAmount();
if (balanceOf(msg.sender) < _amount) revert InsufficientBalance();

_burn(msg.sender, _amount);
emit ERC20SupraToNative(msg.sender, _amount);

(bool sent, ) = payable(msg.sender).call{value: _amount}("");
if (!sent) revert TransferFailed();
}

/// @notice Allows a user to send native tokens directly and get ERC20Supra.
receive() external payable {
Copy link

Choose a reason for hiding this comment

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

what are the use-cases of the reveive() in automation context ?

Choose a reason for hiding this comment

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

@aregng my understanding is that this is not automation specific. A user can send native token to a contract via native value transfer, which will result in invocation of payable method of the target contract. @udityadav-supraoracles to confirm. However, question is, why should we have two methods then for Native 2 ERC20 conversion?

Choose a reason for hiding this comment

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

So, if an EOA sends native token directly to smart contract addressreceive gets executed and it'll mint ERC20Supra tokens.
Either they can send native token directly or they can invoke function deposit(). We can restrict to only deposit if you say.

Copy link

Choose a reason for hiding this comment

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

I do not have any particular opinion on this , so let's keep both unless there is a valid reason not to have any of them.

if (msg.value == 0) revert InvalidAmount();

_mint(msg.sender, msg.value);
emit NativeToERC20Supra(msg.sender, msg.value);
}

/// @notice Disallows sending tokens to the token contract itself. This prevents accidental locking of tokens.
function _update(address _from, address _to, uint256 _value) internal override {
if (_to == address(this)) revert InvalidTransfer();
super._update(_from, _to, _value);
}
}
223 changes: 223 additions & 0 deletions solidity/supra_contracts/test/ERC20Supra.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import {Test} from "forge-std/Test.sol";
import {ERC20Supra} from "../src/ERC20Supra.sol";

contract ERC20SupraTest is Test {
ERC20Supra token;

address owner = address(0x123);
address alice = address(0x456);
address bob = address(0x789);

function setUp() public {
vm.deal(alice, 100 ether);
vm.deal(bob, 50 ether);
vm.deal(owner, 10 ether);

token = new ERC20Supra(owner);
}

function testDeployment() public view {
assertEq(token.owner(), owner);
assertEq(token.name(), "ERC20Supra");
assertEq(token.symbol(), "SUPRA");
assertEq(token.decimals(), 18);

Choose a reason for hiding this comment

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

@so-schen , do we have 18 decimal precision for native $SUPRA on EVM? (CC: @udityadav-supraoracles @aregng )

Copy link

Choose a reason for hiding this comment

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

Yes , according to the following comment and normalization equation between native-coins of VMs.
https://github.com/Entropy-Foundation/smr-moonshot/blob/e2e69c2f7f81b3bc38cf960a133d49f65ba6de97/crates/chainspec/src/lib.rs#L9

}

function testNativeToErc20Supra() public {
vm.prank(alice);
token.nativeToErc20Supra{value: 5 ether}();

assertEq(token.balanceOf(alice), 5 ether);
assertEq(address(token).balance, 5 ether);
assertEq(address(token).balance, token.totalSupply());
assertEq(alice.balance, 95 ether);
}

function testNativeToErc20SupraRevertsIfAmountZero() public {
vm.expectRevert(ERC20Supra.InvalidAmount.selector);

vm.prank(alice);
token.nativeToErc20Supra{value: 0}();
}

function testReceiveMintsERC20Supra() public {
vm.prank(alice);
(bool success, ) = address(token).call{value: 3 ether}("");
require(success);

assertEq(token.balanceOf(alice), 3 ether);
assertEq(address(token).balance, 3 ether);
assertEq(alice.balance, 97 ether);
}

function testReceiveRevertsIfAmountZero() public {
vm.expectRevert(ERC20Supra.InvalidAmount.selector);

vm.prank(alice);
address(token).call{value: 0}("");
}

function testErc20SupraToNative() public {
// Alice deposits 5 SUPRA → gets 5 * 10 ** 18 ERC20Supra tokens
testNativeToErc20Supra();

// Alice withdraws 3 SUPRA → burns 3 * 10 ** 18 ERC20Supra tokens
vm.prank(alice);
token.erc20SupraToNative(3 ether);

assertEq(token.balanceOf(alice), 2 ether);
assertEq(address(alice).balance, 98 ether);
assertEq(address(token).balance, 2 ether);
assertEq(address(token).balance, token.totalSupply());
}

function testErc20SupraToNativeRevertsIfInsufficientBalance() public {
vm.expectRevert(ERC20Supra.InsufficientBalance.selector);

vm.prank(alice);
token.erc20SupraToNative(1 ether);
}

function testErc20SupraToNativeRevertsIfAmountZero() public {
vm.expectRevert(ERC20Supra.InvalidAmount.selector);

vm.prank(alice);
token.erc20SupraToNative(0);
}

function testErc20SupraToNativeRevertsIfNativeTransferFails() public {
// Mint tokens
vm.prank(alice);
token.nativeToErc20Supra{value: 1 ether}();

RejectReceive rejector = new RejectReceive();

// Transfer tokens to the rejecting contract
vm.prank(alice);
token.transfer(address(rejector), 1 ether);

// Attempt withdrawal → should revert
vm.expectRevert(ERC20Supra.TransferFailed.selector);

vm.prank(address(rejector));
token.erc20SupraToNative(1 ether);

assertEq(token.balanceOf(address(rejector)), 1 ether);
}

function testCannotTransferToContract() public {
vm.prank(alice);
token.nativeToErc20Supra{value: 1 ether}();

vm.expectRevert(ERC20Supra.InvalidTransfer.selector);

vm.prank(alice);
token.transfer(address(token), 1 ether);
}

function testMintToContractReverts() public {
vm.deal(address(token), 1 ether);

vm.expectRevert(ERC20Supra.InvalidTransfer.selector);

vm.prank(address(token));
token.nativeToErc20Supra{value: 1 ether}();
}

// Additional test cases for ERC20Supra
function testTransferBetweenUsers() public {
vm.prank(alice);
token.nativeToErc20Supra{value: 5 ether}();

assertEq(token.balanceOf(alice) , 5 ether);

vm.prank(alice);
token.transfer(bob, 2 ether);

assertEq(token.balanceOf(alice), 3 ether);
assertEq(token.balanceOf(bob), 2 ether);
}

function testTransferFromAllowance() public {
vm.prank(alice);
token.nativeToErc20Supra{value: 5 ether}();

vm.prank(alice);
token.approve(bob, 3 ether);

vm.prank(bob);
token.transferFrom(alice, bob, 2 ether);

assertEq(token.balanceOf(alice), 3 ether);
assertEq(token.balanceOf(bob), 2 ether);
assertEq(token.allowance(alice, bob), 1 ether);
}

function testBurnFromReducesBalance() public {
vm.prank(alice);
token.nativeToErc20Supra{value: 5 ether}();

vm.prank(alice);
token.approve(bob, 3 ether);

vm.prank(bob);
token.burnFrom(alice, 2 ether);

assertEq(token.balanceOf(alice), 3 ether);
assertEq(token.allowance(alice, bob), 1 ether);
assertEq(token.totalSupply(), 3 ether);
}

function testTotalSupplyEqualsContractBalance() public {
vm.prank(alice);
token.nativeToErc20Supra{value: 3 ether}();
vm.prank(bob);
token.nativeToErc20Supra{value: 2 ether}();

vm.prank(alice);
token.erc20SupraToNative(1 ether);
vm.prank(bob);
token.erc20SupraToNative(2 ether);

assertEq(address(token).balance, token.totalSupply());
assertEq(token.totalSupply(), 2 ether);
assertEq(token.balanceOf(alice), 2 ether);
assertEq(token.balanceOf(bob), 0);
}

function testNativeToErc20SupraEmitsEvent() public {
vm.expectEmit(true, true, false, false);
emit ERC20Supra.NativeToERC20Supra(alice, 5 ether);

vm.prank(alice);
token.nativeToErc20Supra{value: 5 ether}();
}

function testReceiveEmitsEvent() public {
vm.expectEmit(true, true, false, false);
emit ERC20Supra.NativeToERC20Supra(alice, 3 ether);

vm.prank(alice);
(bool success, ) = address(token).call{value: 3 ether}("");
require(success);
}

function testErc20SupraToNativeEmitsEvent() public {
vm.prank(alice);
token.nativeToErc20Supra{value: 5 ether}();

vm.expectEmit(true, true, false, false);
emit ERC20Supra.ERC20SupraToNative(alice, 2 ether);

vm.prank(alice);
token.erc20SupraToNative(2 ether);
}
}

contract RejectReceive {
fallback() external payable { revert(); }
receive() external payable { revert(); }
}