Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add nft and token linker contracts #144

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
11 changes: 11 additions & 0 deletions examples/evm/nft-linker/IERC721MintableBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IERC721 } from '@openzeppelin/contracts/token/ERC721/IERC721.sol';

interface IERC721MintableBurnable is IERC721 {
function mint(address to, uint256 tokenId) external;

function burn(uint256 tokenId) external;
}
29 changes: 6 additions & 23 deletions examples/evm/nft-linker/NftLinker.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import { ERC721 } from '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import { IERC721 } from '@openzeppelin/contracts/interfaces/IERC721.sol';
import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
Expand Down Expand Up @@ -30,12 +31,7 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//The main function users will interract with.
function sendNFT(
address operator,
uint256 tokenId,
string memory destinationChain,
address destinationAddress
) external payable {
function sendNFT(address operator, uint256 tokenId, string memory destinationChain, address destinationAddress) external payable {
//If we are the operator then this is a minted token that lives remotely.
if (operator == address(this)) {
require(ownerOf(tokenId) == _msgSender(), 'NOT_YOUR_TOKEN');
Expand All @@ -47,11 +43,7 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//Burns and sends a token.
function _sendMintedToken(
uint256 tokenId,
string memory destinationChain,
address destinationAddress
) internal {
function _sendMintedToken(uint256 tokenId, string memory destinationChain, address destinationAddress) internal {
_burn(tokenId);
//Get the original information.
(string memory originalChain, address operator, uint256 originalTokenId) = abi.decode(
Expand All @@ -68,12 +60,7 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//Locks and sends a token.
function _sendNativeToken(
address operator,
uint256 tokenId,
string memory destinationChain,
address destinationAddress
) internal {
function _sendNativeToken(address operator, uint256 tokenId, string memory destinationChain, address destinationAddress) internal {
//Create the payload.
bytes memory payload = abi.encode(chainName, operator, tokenId, destinationAddress);
string memory stringAddress = address(this).toString();
Expand All @@ -84,11 +71,7 @@ contract NftLinker is ERC721, AxelarExecutable, Upgradable {
}

//This is automatically executed by Axelar Microservices since gas was payed for.
function _execute(
string calldata, /*sourceChain*/
string calldata sourceAddress,
bytes calldata payload
) internal override {
function _execute(string calldata /*sourceChain*/, string calldata sourceAddress, bytes calldata payload) internal override {
//Check that the sender is another token linker.
require(sourceAddress.toAddress() == address(this), 'NOT_A_LINKER');
//Decode the payload.
Expand Down
51 changes: 51 additions & 0 deletions examples/evm/nft-linker/NftLinkerBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { AddressToString, StringToAddress } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/AddressString.sol';
import { Upgradable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol';

abstract contract NftLinkerBase is AxelarExecutable, Upgradable {
using StringToAddress for string;
using AddressToString for address;

bytes32 internal constant CONTRACT_ID = keccak256('nft-linker');
IAxelarGasService public immutable gasService;

constructor(address gatewayAddress, address gasServiceAddress_) AxelarExecutable(gatewayAddress) Upgradable() {
gasService = IAxelarGasService(gasServiceAddress_);
}

function contractId() external pure override returns (bytes32) {
return CONTRACT_ID;
}

function sendNft(string memory destinationChain, address to, uint256 tokenId, address refundAddress) external payable virtual {
string memory thisAddress = address(this).toString();
_takeNft(msg.sender, tokenId);
bytes memory payload = abi.encode(to, tokenId);
if (msg.value > 0) {
gasService.payNativeGasForContractCall{ value: msg.value }(
address(this),
destinationChain,
thisAddress,
payload,
refundAddress
);
}
gateway.callContract(destinationChain, thisAddress, payload);
}

function _execute(string calldata /*sourceChain*/, string calldata sourceAddress, bytes calldata payload) internal override {
if (sourceAddress.toAddress() != address(this)) return;
(address recipient, uint256 tokenId) = abi.decode(payload, (address, uint256));
_giveNft(recipient, tokenId);
}

function _giveNft(address to, uint256 tokenId) internal virtual;

function _takeNft(address from, uint256 tokenId) internal virtual;
}
39 changes: 39 additions & 0 deletions examples/evm/nft-linker/NftLinkerLockUnlock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IERC721 } from '@openzeppelin/contracts/interfaces/IERC721.sol';
import { NftLinkerBase } from './NftLinkerBase.sol';

contract NftLinkerLockUnlock is NftLinkerBase {
error TransferFailed();
error TransferFromFailed();

address public immutable operatorAddress;

constructor(
address gatewayAddress_,
address gasServiceAddress_,
address operatorAddress_
) NftLinkerBase(gatewayAddress_, gasServiceAddress_) {
operatorAddress = operatorAddress_;
}

function _giveNft(address to, uint256 tokenId) internal override {
(bool success, bytes memory returnData) = operatorAddress.call(
abi.encodeWithSelector(IERC721.transferFrom.selector, address(this), to, tokenId)
);
bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool)));

if (!transferred || operatorAddress.code.length == 0) revert TransferFailed();
}

function _takeNft(address from, uint256 tokenId) internal override {
(bool success, bytes memory returnData) = operatorAddress.call(
abi.encodeWithSelector(IERC721.transferFrom.selector, from, address(this), tokenId)
);
bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool)));

if (!transferred || operatorAddress.code.length == 0) revert TransferFromFailed();
}
}
39 changes: 39 additions & 0 deletions examples/evm/nft-linker/NftLinkerMintBurn.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IERC721MintableBurnable } from './IERC721MintableBurnable.sol';
import { NftLinkerBase } from './NftLinkerBase.sol';

contract NftLinkerMintBurn is NftLinkerBase {
error TransferFailed();
error TransferFromFailed();

address public immutable operatorAddress;

constructor(
address gatewayAddress_,
address gasServiceAddress_,
address operatorAddress_
) NftLinkerBase(gatewayAddress_, gasServiceAddress_) {
operatorAddress = operatorAddress_;
}

function _giveNft(address to, uint256 tokenId) internal override {
(bool success, bytes memory returnData) = operatorAddress.call(
abi.encodeWithSelector(IERC721MintableBurnable.mint.selector, to, tokenId)
);
bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool)));

if (!transferred || operatorAddress.code.length == 0) revert TransferFailed();
}

function _takeNft(address /*from*/, uint256 tokenId) internal override {
(bool success, bytes memory returnData) = operatorAddress.call(
abi.encodeWithSelector(IERC721MintableBurnable.burn.selector, tokenId)
);
bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool)));

if (!transferred || operatorAddress.code.length == 0) revert TransferFromFailed();
}
}
18 changes: 18 additions & 0 deletions examples/evm/nft-linker/NftLinkerProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { Proxy } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Proxy.sol';

contract NftLinkerProxy is Proxy {
bytes32 internal constant CONTRACT_ID = keccak256('nft-linker');

constructor(address implementationAddress, address owner, bytes memory setupParams) Proxy(implementationAddress, owner, setupParams) {}

// slither-disable-next-line dead-code
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// slither-disable-next-line dead-code

add the same slither/solhint config to this repo?

Copy link
Author

Choose a reason for hiding this comment

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

added solhint/slither configs to ignore these warnings

function contractId() internal pure override returns (bytes32) {
return CONTRACT_ID;
}

receive() external payable override {}
}
157 changes: 157 additions & 0 deletions examples/evm/nft-linker/Ownable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
'use strict';

const chai = require('chai');
const { expect } = chai;
const { ethers } = require('hardhat');
const {
constants: { AddressZero },
} = require('ethers');

describe('Ownable', () => {
let ownableTestFactory;
let ownableTest;

let ownerWallet;
let userWallet;

before(async () => {
[ownerWallet, userWallet] = await ethers.getSigners();

ownableTestFactory = await ethers.getContractFactory(
'TestOwnable',
ownerWallet,
);
});

beforeEach(async () => {
ownableTest = await ownableTestFactory
.deploy(ownerWallet.address)
.then((d) => d.deployed());
});

it('should set the initial owner and return the current owner address', async () => {
const currentOwner = await ownableTest.owner();

expect(currentOwner).to.equal(ownerWallet.address);
});

it('should revert when non-owner calls only owner function', async () => {
const num = 5;

await expect(
ownableTest.connect(userWallet).setNum(num),
).to.be.revertedWithCustomError(ownableTest, 'NotOwner');
});

it('should not revert when owner calls only owner function', async () => {
const num = 5;

await expect(ownableTest.connect(ownerWallet).setNum(num))
.to.emit(ownableTest, 'NumAdded')
.withArgs(num);
});

it('should revert on transfer owner if not called by the current owner', async () => {
const newOwner = userWallet.address;

await expect(
ownableTest.connect(userWallet).transferOwnership(newOwner),
).to.be.revertedWithCustomError(ownableTest, 'NotOwner');
});

it('should revert on transfer owner if new owner address is invalid', async () => {
const newOwner = AddressZero;

await expect(
ownableTest.connect(ownerWallet).transferOwnership(newOwner),
).to.be.revertedWithCustomError(ownableTest, 'InvalidOwnerAddress');
});

it('should transfer ownership in one step', async () => {
const newOwner = userWallet.address;

await expect(ownableTest.transferOwnership(newOwner))
.to.emit(ownableTest, 'OwnershipTransferred')
.withArgs(newOwner);

const currentOwner = await ownableTest.owner();

expect(currentOwner).to.equal(userWallet.address);
});

it('should revert on propose owner if not called by the current owner', async () => {
const newOwner = userWallet.address;

await expect(
ownableTest.connect(userWallet).proposeOwnership(newOwner),
).to.be.revertedWithCustomError(ownableTest, 'NotOwner');
});

it('should propose new owner', async () => {
const newOwner = userWallet.address;

await expect(ownableTest.proposeOwnership(newOwner))
.to.emit(ownableTest, 'OwnershipTransferStarted')
.withArgs(newOwner);
});

it('should return pending owner', async () => {
const newOwner = userWallet.address;

await expect(ownableTest.proposeOwnership(newOwner))
.to.emit(ownableTest, 'OwnershipTransferStarted')
.withArgs(newOwner);

const pendingOwner = await ownableTest.pendingOwner();

expect(pendingOwner).to.equal(userWallet.address);
});

it('should revert on accept ownership if caller is not the pending owner', async () => {
const newOwner = userWallet.address;

await expect(ownableTest.proposeOwnership(newOwner))
.to.emit(ownableTest, 'OwnershipTransferStarted')
.withArgs(newOwner);

await expect(
ownableTest.connect(ownerWallet).acceptOwnership(),
).to.be.revertedWithCustomError(ownableTest, 'InvalidOwner');
});

it('should accept ownership', async () => {
const newOwner = userWallet.address;

await expect(ownableTest.proposeOwnership(newOwner))
.to.emit(ownableTest, 'OwnershipTransferStarted')
.withArgs(newOwner);

await expect(ownableTest.connect(userWallet).acceptOwnership())
.to.emit(ownableTest, 'OwnershipTransferred')
.withArgs(newOwner);

const currentOwner = await ownableTest.owner();

expect(currentOwner).to.equal(userWallet.address);
});

it('should revert on accept ownership if transfer ownership is called first', async () => {
const newOwner = userWallet.address;

await expect(ownableTest.proposeOwnership(newOwner))
.to.emit(ownableTest, 'OwnershipTransferStarted')
.withArgs(newOwner);

await expect(ownableTest.transferOwnership(newOwner))
.to.emit(ownableTest, 'OwnershipTransferred')
.withArgs(newOwner);

const currentOwner = await ownableTest.owner();

expect(currentOwner).to.equal(userWallet.address);

await expect(
ownableTest.connect(userWallet).acceptOwnership(),
).to.be.revertedWithCustomError(ownableTest, 'InvalidOwner');
});
});
Loading
Loading